Избегание клавиатуры в фокусе UITextField
Пара сообщения назад я писал об автоматической обработке кнопки «Далее». В этом посте я хотел бы написать об автоматическом отказе от использования клавиатуры таким образом, чтобы обеспечить как удобство работы пользователя, так и удобство работы разработчика.
Большинство приложений имеют немного этакая форма, которую требуется заполнить, пусть даже просто логин/регистрация, а то и несколько. Меня как пользователя огорчает то, что клавиатура закрывает текстовое поле, которое я собираюсь заполнить; это плохой пользовательский опыт. Как разработчики, мы хотели бы решить эту проблему как можно проще и сделать решение максимально пригодным для повторного использования.
Что означает хороший пользовательский опыт?
- Сфокусированный
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
вставку и смещение при отпускании клавиатуры — и мы, скорее всего, реализуем это в UIViewController
тем не мение.
Протокол также должен иметь 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
}
}
Теперь мы обновим расширение протокола с помощью Self: UIViewController
ограничение (1), потому что нам понадобится доступ к окну. Это не должно быть неудобством, потому что этот протокол, скорее всего, будет использоваться UIViewController
s, но другим подходом будет замена всех view.window
происшествия с UIApplication.shared.keyWindow
или вариация UIApplication.shared.windows[yourIndex]
если у вас сложная иерархия.
Затем мы вычисляем minY
для клавиатуры (2) — используем параметр для тех случаев, когда у нас есть кастомный inputView
и мы будем называть это из textFieldShouldBeginEditing(_:)
, Например. Затем мы проверяем, является ли наш preKeyboard
свойства nil
и если они есть, мы присваиваем текущие значения из scrollView
(3, 4); их может не быть nil
если мы изменили их до вызова этого метода.
Затем мы конвертируем textField
х 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).
Использование этого может варьироваться в зависимости от ваших потребностей, но обычный сценарий будет выглядеть так:
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()
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil) { [weak self] notification in
self?.keyboardChanged(with: notification)
}
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil) { [weak self] notification in
self?.keyboardChanged(with: notification)
}
// And that's it!
}
// [...]
}
Это был длинный пост, но теперь у нас есть хорошая логика «держать текстовое поле над клавиатурой», и если мы реализуем все это вместе с автоматическая обработка кнопки «Далее»это будет похоже на волшебство для наших пользователей.
Проверить эта почта о небольшой автоматизации этого еще дальше, внедрив систему Broadcaster/Listener и переместив наблюдателей в Broadcaster
сам. Нам больше не нужно было бы добавлять наблюдателей в наши контроллеры представлений, нам просто нужно было бы вызвать Broadcaster.shared.addListener(self)
.
Как обычно, дайте мне знать, если есть что-то, что можно улучшить. @роландлет.
Вы можете найти больше подобных статей в моем блоге или подписаться на мой ежемесячный Новостная рассылка. Первоначально опубликовано на https://rolandleth.com.