Работа с клавиатурой в iOS: как минимизировать копипасту

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

Кто разрабатывал приложение под 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.

Источник: https://habr.com/ru/company/surfstudio/blog/573870/


Интересные статьи

Интересные статьи

Привет! Это вторая часть статьи, в которой мы будем разбирать практическое применение платформы Graylog.В первой части мы разобрали как платформу установить и произвести ...
В 2017 году студенты Университета Аризоны Алан Алчалел и Брэди Сильвервуд разработали стратегию продвижения своей линии купальников Sunny Co Clothing. Они обещали всем, кто сделает репост...
В любом приложении, состоящем более чем из одного экрана, существует необходимость реализовать навигацию между его компонентами. Казалось бы, это не должно быть проблемой, ведь в UIKit есть дост...
КДПВ взята отсюда Часто слышу истории вида "пробовал фрилансить — не понравилось" и встречаю много заблуждений по поводу этого типа работ, потому что люди просто начали "не с той стороны". Поста...
Работа над любым исследовательским проектом включает в себя поиск и изучение множества источников информации. Организация этого процесса — непростая задача. Сегодня мы расскажем об инструментах, ...