Автоматическая обработка кнопки Далее
Ввод текста в несколько текстовых полей является такой распространенной схемой - везде, не только в iOS - должен быть способ легко переходить от одного поля к другому, желательно «правильному». К сожалению, iOS не предлагает эту функцию, но давайте посмотрим, как мы можем сделать это сами.
Во-первых, краткий обзор того, что нам нужно:
- См. Кнопку «Далее», если после текущего поля есть какие-либо поля; желательно только те, которые еще пусты.
- Нажав кнопку «Далее», мы перейдем в нужное поле.
- См. Кнопку «Возврат», если после текущего поля больше нет; желательно с учетом пустых.
- Нажатие кнопки Return увольняет текущего респондента.
- Все должно работать автоматически, независимо от вида и количества полей.
- Вероятно, все должно быть инкапсулировано в протокол.
Круто, давайте перейдем к протоколу. Что нам здесь нужно?
- Список текстовых полей (1).
- Метод, обрабатывающий начало редактирования текстовых полей для отображения правильной кнопки (2).
- Метод, обрабатывающий нажатие конца / возврата при редактировании текстовых полей для выполнения правильного действия (3).
protocol NextTextFieldHandler {
var textFields: [UITextField] { get } // 1 func setupReturnKeyType(for textField: UITextField) // 2 func handleReturnKeyTapped(on textField: UITextField) // 3
}
Свойство textFields
(1) может содержать либо все поля в представлении, либо только те поля, по которым мы заинтересованы в навигации - протоколу это не важно, наша задача - решить, что необходимо, при соблюдении протокол. Эти две функции используются для удовлетворения двух других наших потребностей (2, 3).
extension NextTextFieldHandler {
private var _textFields: [UITextField] {
return textFields.filter { !$0.isHidden && $0.alpha != 0 && $0.isEnabled } // 1 } }
Мы могли бы добавить некоторые меры безопасности, на случай, если мы не будем осторожны, что также будет действовать для удобства: эти условия придется записывать снова и снова, чтобы мы могли автоматически отфильтровать скрытые поля и отключенные (1) .
Но, как указано выше, протокол не должен заботиться о том, что входит в textFields
; по какой-то причине мы могли бы захотеть включить отключенное текстовое поле, и у нас не было бы возможности сделать это. Если вы действительно знаете, что в проекте нет странных сценариев, вы можете добавить указанное выше свойство и использовать его вместо textFields
.
Далее нам нужен способ извлечь все поля после текущего:
extension NextTextFieldHandler {
private func fields(after textField: UITextField) -> ArraySlice<UITextField> {
let textFields = self.textFields // 1 guard let currentIndex = textFields.index(of: textField) else { return [] } // 2 return textFields.suffix(from: min(currentIndex + 1, textFields.endIndex - 1)) // 3 } }
Сначала мы сохраняем массив в локальной константе для небольшого повышения эффективности (1), поскольку свойство textFields
может быть вычисленным. В некоторых текстовых полях это усиление незначительно, но все же рекомендуется не перебирать вычисляемые свойства - особенно при итерации - если нам действительно не нужно, чтобы свойство пересчитывалось снова и снова, что нам не нужно.
Затем мы находим индекс переданного текстового поля или спасаемся, если не можем его найти. Наконец, мы возвращаем ArraySlice
со всеми текстовыми полями, начиная с поля, следующего за текущим.
Зачем нужен ломтик? Потому что сохраняет оригинальные индексы. Так, например, если у нас есть [field1, field2, field3, field4]
и мы делаем срез, начиная с индекса 2, мы получим [field3, field4]
, но каждый элемент будет иметь следующие индексы: [2, 3]
. Мы чуть позже увидим, зачем это нужно.
И последнее, прежде чем мы перейдем к двум функциям: простой помощник для проверки наличия пустых полей в приведенном выше фрагменте:
extension NextTextFieldHandler {
private func emptyFieldsExist(after textField: UITextField) -> Bool {
return fields(after: textField) .filter { $0.text?.isEmpty != false } .isEmpty == false } }
Он берет только поля после текущего, фильтрует то, в котором есть текст, и проверяет, является ли результат пустым.
Наконец, мы можем приступить к реализации:
extension NextTextFieldHandler {
func setupReturnKeyType(for textField: UITextField) {
let textFields = self.textFields // 1 guard let currentIndex = textFields.index(of: textField) else { return } // 2 let emptyFieldsExistAfterCurrent = emptyFieldsExist(after: textField) if currentIndex < textFields.endIndex - 1, emptyFieldsExistAfterCurrent { // 2 textField.returnKeyType = .next } else { // 3 textField.returnKeyType = .done } } }
Как и раньше, мы сохраняем массив textFields
в локальной константе (1) и спасаемся, если не можем найти индекс переданного в textField
(2).
Затем мы должны принять решение:
- Если мы не на последнем
textField
и у нас есть пустые поля после текущего, ключ возврата должен быть.next
(2). - Если мы находимся на последнем
textField
или все поля после текущего заполнены, ключ возврата должен быть.done
(3).
Наконец, логика нажатия клавиши Next / Return немного сложнее, и мы наконец поймем, почему мы использовали ArraySlice
выше:
extension NextTextFieldHandler {
func handleReturnKeyTapped(on textField: UITextField) {
let textFields = self.textFields // 1 guard let currentIndex = textFields.index(of: textField) else { return } // 2 let fieldsAfterCurrent = fields(after: textField) // 3 let nextEmptyIndex = fieldsAfterCurrent .firstIndex { $0.text?.isEmpty != false } // 4 ?? textFields.index(currentIndex, offsetBy: 1, limitedBy: textFields.endIndex - 1) // 5 ?? textFields.endIndex - 1 // 6 let emptyFieldsExistAfterCurrent = emptyFieldsExist(after: textField) if currentIndex == textFields.endIndex - 1 || !emptyFieldsExistAfterCurrent { // 7 textField.resignFirstResponder() } else { // 8 textFields[nextEmptyIndex].becomeFirstResponder() } } }
Как всегда, мы сначала сохраняем массив textFields
в локальной константе (1) и выходим, если не можем найти индекс переданного в textField
(2). Затем мы получаем следующие поля после того, которое было передано как срез (3), и находим индекс первого пустого поля (4), возвращаясь к следующему текстовому полю, если это не удается (5), снова возвращаясь к последнее текстовое поле, если это тоже не удается (6).
Теперь нам нужно принять другое решение:
- Если мы находимся на последнем
textField
или во всех полях после заполнения текущего, уволите первого респондента (7). - Если мы не на последнем
textField
и у нас есть пустые поля после текущего, сфокусируйтесь на первом пустомtextField
после текущего (8).
И вот здесь нам нужен ArraySlice
. Давайте рассмотрим предыдущий пример:
textFields = [field1, field2, field3, field4]
.- Мы на странице
field1
, и первое пустое текстовое поле - этоfield3
с индексом 2. fields(after: field1)
возвращает[field2, field3, field4]
с индексами[1, 2, 3]
.firstIndex { $0.text?.isEmpty != false }
(4) вернет индекс2
, как и должно.textFields[nextEmptyIndex].becomeFirstResponder()
сфокусируется на правильном поле,field3
.
Если бы мы не использовали срез, fields(after: field)
все равно вернул бы field2, field3, field4]
, но с индексами [0, 1, 2]
. Кроме того, мы не используем filter
внутри fields(after:)
, потому что это приведет к сбросу индексов, заставляя их начинать с 0
.
Если бы мы не использовали срез, мы могли бы также найти field3
, используя first { $0.text?.isEmpty != false }
в (4), тогда мы бы нашли его индекс, вызвав textFields.index(of: nextField)
, но я думаю, что срез немного лучше, поскольку мы можем писать более простые резервные варианты (5, 6).
Как мы его используем? На самом деле довольно просто:
extension LoginViewController: UITextFieldDelegate, NextTextFieldHandler {
var textFields: [UITextField] { // 1
return aStackView.arrangedSubviews .compactMap { $0 as? UITextField } .filter { !$0.isHidden && $0.alpha != 0 && $0.isEnabled } // 2 /* 3 if mode == .login { return [emailTextField, passwordTextField] } else { return [nameTextField, emailTextField, passwordTextField] } */ } func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { setupReturnKeyType(for: textField) // 4 return true } func textFieldShouldReturn(_ textField: UITextField) -> Bool { handleReturnKeyTapped(on: textField) // 5 return true } }
Сначала мы устанавливаем нужный нам textFields
(1). Мы извлекаем все текстовые поля из нашего воображаемого UIStackView
(2) (или возвращаем их вручную - 3), затем вызываем setupReturnKeyType(for:)
из textFieldShouldBeginEditing(:_)
(4) и handleReturnKeyTapped(on:)
из textFieldShouldReturn(:_)
(5).
Как всегда, я хотел бы знать, что вы думаете, или можно ли что-нибудь улучшить @rolandleth.
Первоначально опубликовано на rolandleth.com.