При разработке практически любого мобильного приложения разработчику придётся столкнуться с полями ввода. А где поля ввода — там и клавиатура, а также логика, связанная с обработкой событий её жизненного цикла: появления, сокрытия, изменения размеров.
Кто разрабатывал приложение под 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.