При разработке практически любого мобильного приложения разработчику придётся столкнуться с полями ввода. А где поля ввода — там и клавиатура, а также логика, связанная с обработкой событий её жизненного цикла: появления, сокрытия, изменения размеров.
Кто разрабатывал приложение под iOS, знает, что работа с клавиатурой — это часть очень похожего или даже одинакового кода, название которому — копипаста. Как мы с ним в Surf боролись и насколько удалось сократить кодовую базу, поговорим в статье.

Предположим, у нас есть экран, на котором расположено поле ввода. При тапе показывается клавиатура. Наша цель — обработать появление и последующее исчезновение клавиатуры с экрана в зависимости от её размеров и времени появления или сокрытия. Код может выглядеть так:
// метод, в котором происходит подписка на события от NotificationCenter func subscribeOnKeyboardNotifications() { let center = NotificationCenter.default center.addObserver(self, selector: #selector(keyboardWillBeShown(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) center.addObserver(self, selector: #selector(keyboardWillBeHidden(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) } // метод, вызываемый при появлении клавиатуры @objc func keyboardWillBeShown(notification: Notification) { // пытаемся получить доступ к высоте клавиатуры и времени анимации guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect, let animationTime = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return } let keyboardHeight = keyboardFrame.height // выполняем код } // метод, вызываемый при сокрытии клавиатуры @objc func keyboardWillBeHidden(notification: Notification) { // пытаемся получить доступ к высоте клавиатуры guard let animationTime = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return } // выполняем код }
Предположим, появился второй экран с полями ввода. Не беда: скопируем уже написанные ранее методы, изменив только обработку событий.
А потом появится третий экран, четвёртый… Думаю, вы уже поняли, к чему я клоню: на всех экранах появится абсолютно одинаковый код, отличающийся только обработкой событий. Высока вероятность, что разработчик будет набирать код не с нуля, а повторять с помощью Cmd+C/Cmd+V.
Это явно намекает нам, что пора задуматься о переиспользовании кода, чтобы избежать копипасты. Так возникла идея написать утилиту, которая бы:
-
Позволяла подписаться на события клавиатуры или отписаться от них.
-
Вызывала заранее определённый метод на события появления и сокрытия клавиатуры.
-
При этом передавала туда не сырой объект Notification, а данные нужного типа: animationDuration, keyboardFrame и так далее.
-
Освобождала от работы с NotificationCenter, позволяя не вспоминать мучительно каждый раз нужные ключи и типы данных.
-
Освобождала от необходимости копипастить код.
Как мы утилиту делали
Первая проблема на пути к чистому коду: как организовать задумку в архитектурном плане? Мы хотим вынести обработку событий клавиатуры за пределы ViewController-а, но при этом необходимо будет вызывать его методы. Возникают вопросы:
-
Где выполнять обработку событий?
-
Как с ViewController-ом связать объект, где будет происходить эта обработка?
-
Как сделать так, чтобы где-нибудь хранилась strong-ссылка на подобный объект?
Базовые классы — не наш подход. Хочется сделать систему гибкой: базовые классы этому способствовать не будут.
Понять первую версию утилиты поможет схема и небольшой листинг ниже:
public protocol KeyboardObservable: class { func subscribeOnKeyboardNotifications() func unsubscribeFromKeyboardNotifications() func keyboardWillBeShown(notification: Notification) func keyboardWillBeHidden(notification: Notification) }
Принцип работы таков:
-
Указываем, что ViewController удовлетворяет протоколу
KeyboardObservable. -
Протокол содержит 4 метода. Два из них — subscribe и unsubscribe — реализованы в дефолтном расширении этого протокола, так что потребности в их реализации нет.
-
В процессе подписки на нотификации создается объект
observer, который содержит слабую ссылку на ViewController. Он будет отвечать за обработку событий появления и сокрытия клавиатуры: именно его методы будут вызываться при срабатывании нотификаций. -
Если клавиатуры покажется или сокроется,
observerвызовет два соответствующих метода у view.
Это решает часть проблемы: теперь нет необходимости реализовывать методы подписки и отписки от нотификаций, так как подобная логика будет реализована в одном месте. Но остается необходимость обработать объект Notification и вытянуть из него необходимые параметры — то есть нужно реализовать два оставшихся метода протокола KeyboardObservable.
Чтобы решить эту проблему, мы предусмотрели протоколы, обозначенные на схеме как <Specific>KeyboardPresentable. Они могут иметь следующий вид:
public protocol CommonKeyboardPresentable: class { func keyboardWillBeShown(keyboardHeight: CGFloat, duration: TimeInterval) func keyboardWillBeHidden(duration: TimeInterval) }
Для применения протокола необходимо указать, что ViewController, помимо KeyboardObservable, удовлетворяет еще и протоколу CommonKeyboardPresentable, и реализовать его методы.
У протокола CommonKeyboardPresentable есть extension, где реализуются два оставшихся метода протокола KeyboardObservable. В момент их вызова из объекта Notification извлекаются необходимые параметры и вызываются соответствующие методы протокола CommonKeyboardPresentablе.
Теперь не нужно копипастить логику обработки полезной нагрузки из нотификации — она будет реализована в одном месте. При этом остаётся возможность расширить механизм и написать собственный <Specific>KeyboardPresentable, в котором методы будут иметь необходимые именно вам параметры.
Отдельного внимания заслуживает способ хранения объекта observer в памяти. На схеме место его хранения обозначено как Pool.
-
Pool— хранилище observer-ов, которое держит на каждый из них strong-ссылку и не даёт уйти из памяти. -
Каждый observer держит weak-ссылку на ViewController, для которого он был создан.
-
Таким образом удалось избежать reference-cycle между ViewController-ом и соответствующим ему observer-ом.
-
Остаётся проблема «бесхозных» observer-ов, когда объект observer будет содержать view == nil. Это кейс, когда ViewController ушел из жизни, а observer остался. Проблема решается путем периодической очистки пула от таких объектов.
В результате получилась гибкая система, которая не требует запоминания большого числа констант и написания больших кусков одинакового кода. Всё, что нужно сделать:
-
Объявить, что ViewController поддерживает протокол KeyboardObservable.
-
Поправить появившиеся в Xcode ошибки, реализовав два метода этого протокола.
-
Либо объявить, что ViewController поддерживает SpecificKeyboardPresentable протокол, и реализовать его методы.
Структура класса может выглядеть так:
final class ViewController: UIViewController, KeyboardObservable { ... } extension ViewController: CommonKeyboardPresentable { func keyboardWillBeShown(keyboardHeight: CGFloat, duration: TimeInterval) { // do something useful } func keyboardWillBeHidden(duration: TimeInterval) { // do something useful } }
При этом в качестве SpecificKeyboardPresentable вы можете использовать уже готовые протоколы, которые содержит утилита (например, CommonKeyboardPresentable), либо написать свой. Достаточно только чтобы он удовлетворял протоколу KeyboardObservable и реализовывал два метода, которые отсутствуют в дефолтной реализации.
Как бонус — структура KeyboardInfo, упрощающая работу со словарем userInfo нотификации:
extension Notification { public struct KeyboardInfo { public var frameBegin: CGRect? public var animationCurve: UInt? public var animationDuration: Double? public var frameEnd: CGRect? } public var keyboardInfo: KeyboardInfo }
Результат: минус сотни строк абсолютно одинакового кода
Благодаря утилите количество кода в рамках одного экрана сократилось незначительно: примерно на 15 строк. Но на приложениях с большим количеством экранов мы удалили порядка 1000 строк абсолютно одинакового кода!
И самое важное: теперь можно не вспоминать каждый раз названия ключей для Notification. Даже названия методов из протоколов помнить необязательно: Xcode предложит вставить объявление пропущенных методов за вас. Всё, что остается, — только добавить реализацию.
Полный код этой и других утилит — в репозитории Surf.
Больше полезного про iOS — в нашем телеграм-канале Surf iOS Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf. Присоединяйтесь >>
ссылка на оригинал статьи https://habr.com/ru/articles/573870/
Добавить комментарий