Как управлять клавиатурой в фокусе UITextField для лучшего использования

1656518169 kak upravlyat klaviaturoj v fokuse uitextfield dlya luchshego ispolzovaniya

автор Роланд Лет

QYUP3zRJGMmDNWF5iPlTYREJ5no5ZkXlwxfK
Авторы: Алесь на Unsplash.com

Несколько сообщений назад я писал об автоматической работе с кнопкой «Далее». В этой публикации я хотел бы написать о том, чтобы избежать автоматического использования клавиатуры, чтобы обеспечить как хороший опыт пользователя, так и хороший опыт разработчика.

В большинстве программ есть некоторые своего рода форма, которую нужно заполнить, даже если это только логин/реестр, если не несколько форм. Мне как пользователю становится грустно, когда клавиатура закрывает текстовое поле, которое я собираюсь заполнить – это плохой опыт пользователя. Как разработчики мы хотели бы решить эту проблему как можно проще и чтобы решение было максимально многократным.

Что означает хороший пользовательский опыт?

  • Сосредоточенный UITextField выводится над клавиатурой на фокусе.
  • Сосредоточенный UITextField «отправляется обратно» при увольнении.

Что означает хороший опыт разработчика? Все должно происходить максимально автоматически, поэтому мы перейдем еще раз к протоколу. Что нужно инкапсулировать в этом протоколе?

  • Наблюдение за клавиатурой отображает или скрывает уведомления.
  • Что касается внешнего вида клавиатуры, ее нужно изменить scrollView.contentInset и scrollView.contentOffset таким образом, что приносит UITextField прямо над клавиатурой.
  • При исчезновении клавиатуры необходимо сбросить вставку и смещение в предыдущие значения.

Имея это в виду, давайте построим наш протокол:

protocol KeyboardListener: AnyObject { // 1
   var scrollView: UIScrollView { get } // 2   var contentOffsetPreKeyboardDisplay: CGPoint? { get set } // 3   var contentInsetPreKeyboardDisplay: UIEdgeInsets? { get set } // 4
   func keyboardChanged(with notification: Notification) // 5
}

Нам нужно ограничить этот протокол, чтобы он соответствовал только классам (1), потому что нам нужно будет сменить два preKeyboard свойства (3, 4). Мы используем их, чтобы знать, как вернуть scrollViewВставка и смещение при закрытии клавиатуры. Скорее всего, мы реализуем это в a UIViewController все равно.

Протокол также должен иметь a scrollView (2), иначе это на самом деле не… возможно (я думаю, что это могло бы быть осуществимо). Наконец нам нужен метод, который будет обрабатывать все (5), но он просто действует как прокси-сервер для двух помощников, которые мы реализуем лишь немного:

extension KeyboardListener {
   func keyboardChanged(with notification: Notification) {      guard         notification.name == UIResponder.keyboardWillShowNotification,         let rawFrameEnd = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey],         let frameEnd = rawFrameEnd as? CGRect,         let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval      else {         resetScrollView() // 1
         return      }
      if let currentTextField = UIResponder.current as? UITextField {         updateContentOffsetOnTextFieldFocus(currentTextField, bottomCoveredArea: frame.height) // 2      }
      scrollView.contentInset.bottom += frameEnd.height // 3   }
}

Если уведомление не для willShowили мы не можем проанализировать уведомления userInfoвыручите и сбросьте scrollView. Если да, увеличьте нижнюю вставку на высоту клавиатуры (3). Что касается (2), мы находим текущего первого ответчика с небольшой хитростью для вызова updateContentOffsetOnTextFieldFocus(_:bottomCoveredArea:) с, но мы также можем вызвать его из нашего делегата textFieldShouldBeginEditing(_:).

Первый помощник обновит наших двоих preKeyboard свойства:

extension KeyboardListener where Self: UIViewController { // 1
   func keyboardChanged(with notification: Notification) {      // [...]   }
   func updateContentOffsetOnTextFieldFocus(_ textField: UITextField, bottomCoveredArea: CGFloat) {      let projectedKeyboardY = view.window!.frame.minY - bottomCoveredArea // 2
      if contentInsetPreKeyboardDisplay == nil { // 3         contentInsetPreKeyboardDisplay = scrollView.contentInset      }      if contentOffsetPreKeyboardDisplay == nil { // 4         contentOffsetPreKeyboardDisplay = scrollView.contentOffset      }
      let textFieldFrameInWindow = view.window!.convert(textField.frame,                                                        from: textField.superview) // 5      let bottomLimit = textFieldFrameInWindow.maxY + 10 // 6
      guard bottomLimit > projectedKeyboardY else { return } // 7
      let delta = projectedKeyboardY - bottomLimit // 8      let newOffset = CGPoint(x: scrollView.contentOffset.x,                              y: scrollView.contentOffset.y - delta) // 9
      scrollView.setContentOffset(newOffset, animated: true) // 10   }
}

Теперь мы обновим расширение протокола с помощью a Self: UIViewController ограничение (1), потому что нам нужен доступ к окну. Это не должно вызывать неудобств, потому что этот протокол, скорее всего, будет использоваться UIViewControllerс. Однако другой подход заключался бы в замене всех view.window явления с UIApplication.shared.keyWindow или вариация UIApplication.shared.windows[yourIndex]если у вас сложная иерархия.

Затем вычисляем minY для клавиатуры (2) — мы используем параметр для тех случаев, когда у нас каст inputView и мы позвоним это из textFieldShouldBeginEditing(_:), например. Затем мы проверяем, наша ли preKeyboard свойства есть nil. Если они есть, мы назначаем текущие значения из scrollView (3, 4). Их может и не быть nil если мы сменили их перед вызовом этого метода.

Затем конвертируем textField‘s maxY в координатах окна (5) и добавьте 10 к нему (6), поэтому мы имеем небольшое отступление между полем и клавиатурой. Если bottomLimit находится над клавиатурой minYничего не делать, потому что textField уже полностью виден (7). Если bottomLimit находится ниже клавиатуры minYвычислите разницу между ними (8), чтобы мы знали, сколько нужно прокручивать scrollView (9, 10) так что textField будет видно.

Второй помощник сбрасывает наш scrollView вернуться к исходным значениям:

extension KeyboardListener where Self: UIViewController {
   func keyboardChanged(with notification: Notification) {      // [...]   }
   func updateContentOffsetOnTextFieldFocus(_ textField: UITextField, bottomCoveredArea: CGFloat) {      // [...]   }
   func resetScrollView() {      guard // 1         let originalInsets = contentInsetPreKeyboardDisplay,         let originalOffset = contentOffsetPreKeyboardDisplay      else { return }
      scrollView.contentInset = originalInsets // 2      scrollView.setContentOffset(originalOffset, animated: true) // 3
      contentInsetPreKeyboardDisplay = nil // 4      contentOffsetPreKeyboardDisplay = nil // 5   }
}

Если у нас нет оригинальных вставок/смещений, ничего не делать; например, используется аппаратная клавиатура (1). Если мы это сделаем, мы сбросим scrollView к исходным значениям, предшествующим клавиатуре (2, 3) и nil— с preKeyboard свойства (4, 5).

qxbcull6EPq0v6FFxYdbKqeDvXKNOvrSyY5T
Авторы: Мервин на Unsplash.com

Использование этого может отличаться в зависимости от ваших потребностей, но обычный сценарий будет выглядеть так:

final class FormViewController: UIViewController, KeyboardListener {
   let scrollView = UIScrollView()      /* Or if you have a tableView:            private let tableView = UITableView()      var scrollView: UIScrollView {         return tableView      }   */
   // [...]
   override func viewDidLoad() {      super.videDidLoad()
      let center = NotificationCenter.default
      center.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil) { [weak self] notification in        self?.keyboardChanged(with: notification)      }
      center.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil) { [weak self] notification in        self?.keyboardChanged(with: notification)      }
      // And that's it!   }
   // [...]
}

Это было много информации, но теперь у нас хорошая логика «держать текстовое поле над клавиатурой». Если мы введем все это вместе с автоматической обработкой кнопки «Далее», это будет как магия для наших пользователей.

Ознакомьтесь с этой публикацией о еще большей автоматизации, введя систему вещателя/слушателя и переместив наблюдателей в Broadcaster себя. Нам больше не нужно будет добавлять наблюдателей в наши контроллеры представления, нам просто нужно будет вызвать Broadcaster.shared.addListener(self).

Как всегда, я хотел бы услышать ваши мысли @rolandleth.

Первоначально опубликовано на rolandleth.com 18 октября 2018 года.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *