Cuando rellenas un formulario en Safari, el teclado tiene un UIToolbar
con dos flechas que te permiten navegar al siguiente UITextField
o volver al anterior. ¿No sería genial tener lo mismo en tu app iOS?
En este tutorial vas a crear un TextFieldNavigator
que te permita navegar a través de los UITextField
s de tu formulario. Éste tendrá tres botones:
- Una flecha hacia arriba: para navegar al campo de texto anterior.
- Una flecha hacia abajo: para navegar al campo de texto siguiente.
- Un botón
"Done"
para cerrar y ocultar el teclado.
Si llegas hasta el final del artículo, podrás tener un formulario con su UIToolbar
como los de Safari, pero en tu aplicación iOS:
becomeFirstResponder
& resignFirstResponder
Antes de comenzar, hay que entender cómo se puede añadir o quitar el foco a un UITextField
.
Que un TextField tenga el foco implica que el teclado está visible y todo lo que se escribe aparece en él. Cuando un TextField pierde el foco, el teclado desaparece.
Para poner el foco manualmente al campo de texto, se utiliza la función becomeFirstResponder()
, e inmediatamente el teclado aparece y se puede escribir en TextField:
someTextField.becomeFirstResponder()
Para quitar el foco, se usa resignFirstResponder()
y el teclado desaparece:
someTextField.resignFirstResponder()
El TextFieldNavigator
que se va a crear jugará con estas dos funciones para navegar (es decir, añadir el foco) al UITextField
correspondiente.
Manos al teclado. Crear el navegador
Crea un nuevo fichero Swift y llámalo TextFieldNavigator
:
import UIKit
class TextFieldNavigator {
}
Paso 1: Configurar la UIToolbar
La UIToolbar
tendrá tres botones: dos flechas para navegar por los TextFields y un botón "Done"
para cerrar y ocultar el teclado.
Añade este código a tu clase:
class TextFieldNavigator {
// MARK: - Toolbar & UIBarButtonItems
private var toolbar: UIToolbar!
private var previousButton: UIBarButtonItem!
private var nextButton: UIBarButtonItem!
private var doneButton: UIBarButtonItem!
// MARK: - Initialization
init() {
setupToolbar()
}
}
Lo que se ha hecho hasta aquí ha sido:
- Guardar como propiedades la
UIToolbar
y los botones, que serán del tipoUIBarButtonItem
. - Crear un
init
y llamar en él al métodosetupToolbar()
, que definirás en un instante.
Ahora vas a configurar la toolbar. Puedes añadir este código en una extensión.
// MARK: - Setup Toolbar
extension TextFieldNavigator {
private func setupToolbar() {
// 1
toolbar = UIToolbar()
// 2
previousButton = UIBarButtonItem(image: UIImage(named: "arrow-up")!, style: .plain, target: self, action: #selector(didTapPreviousTextField))
nextButton = UIBarButtonItem(image: UIImage(named: "arrow-down")!, style: .plain, target: self, action: #selector(didTapNextTextField))
doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(didTapDone))
let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
// 3
toolbar.setItems([previousButton, nextButton, spaceButton, doneButton], animated: false)
toolbar.sizeToFit()
}
}
Vamos por partes:
// 1:
Creas laUIToolbar
// 2:
Creas los botones, asignándole las imágenes de las flechas y el#selector
que se ejecutará con cada uno. Declararás estos métodos en un minuto.// 3:
Añades los botones a la toolbar con unspaceButton
entre las flechas y el botón"Done"
. Por último, llamas asizeToFit()
para que la toolbar se ajuste a todos sus botones.
Ahora vas a crear los métodos que se ejecutarán cuando se pulsen los botones, pero de momento los dejarás todos vacíos. Añade este código debajo del método setupToolbar()
:
@objc private func didTapDone() {
// Empty, for now
}
@objc private func didTapPreviousTextField() {
// Empty, for now
}
@objc private func didTapNextTextField() {
// Empty, for now
}
Paso 2: Añadir los UITextField
¿Qué es un TextFieldNavigator
sin text fields? En este punto vas a añadir la magia necesaria para navegar entre los UITextFields
.
class TextFieldNavigator {
// MARK: - Toolbar & UIBarButtonItems
...
// MARK: - Properties
// 1
private var textFields: [UITextField]
private weak var activeTextField: UITextField?
// MARK: - Initialization
// 2
init(with textFields: [UITextField]) {
self.textFields = textFields
setupToolbar()
}
}
//1:
Guardas un array deUITextFields
para ir navegando entre ellos, y una propiedad para guardar el TextField que está activo (es decir, el que tiene el foco). Éste debe ser<a href="https://alexandrefreire.com/arc-swift/weak-vs-unowned-en-swift/">weak</a>
para evitar posibles referencias circulares.// 2:
Actualizas elinit
de manera que acepte un array deUITextFields
como parámetro de entrada.
Ahora vas a crear un par de propiedades de utilidad para saber si el TextField que está activo es el primero o el último. Añade, debajo de las propiedades declaradas anteriormente, el siguiente código:
class TextFieldNavigator {
// MARK: - Toolbar & UIBarButtonItems
...
// MARK: - Properties
...
private var isFirstTextFieldActive: Bool {
return activeTextField == textFields.first
}
private var isLastTextFieldActive: Bool {
return activeTextField == textFields.last
}
// MARK: - Initialization
...
}
Paso 3: Subscribirse a las notificaciones de UITextField
Ahora tienes que subscribirte a la notificación UITextField.textDidBeginEditingNotification
para enterarte de que el TextField está siendo editado. Crea los siguientes métodos en una extensión:
// MARK: - Notifications
extension TextFieldNavigator {
private func subscribeToNotifications() {
// 1
NotificationCenter.default.addObserver(
self,
selector: #selector(textFieldDidBeginEditing),
name: UITextField.textDidBeginEditingNotification,
object: nil
)
}
private func unsubscribeToNotifications() {
// 2
NotificationCenter.default.removeObserver(self)
}
@objc private func textFieldDidBeginEditing(notification: NSNotification) {
// 3
guard let textField = notification.object as? UITextField,
textFields.contains(textField) else {
return
}
// 4
activeTextField = textField
// 5
previousButton.isEnabled = !isFirstTextFieldActive
nextButton.isEnabled = !isLastTextFieldActive
}
}
// 1:
Creas un método para subscribirse a la notificaciónUITextField.textDidBeginEditingNotification
, que notifica que la edición en un TextField acaba de comenzar.// 2:
Creas un método para de-subscribirte a las notificaciones.// 3:
Extraes el TextField que está siendo editado y compruebas que éste está incluido en el arraytextFields
.// 4:
Asignas el TextField que se está editando como elactiveTextField
.// 5:
Deshabilitas elpreviousButton
si el TextField activo es el primero, y elnextButton
si es el último.
Por último, tienes que llamar a estos métodos para subscribirse y de-subscribirse en el init
y deinit
respectivamente. Añade este código:
class TextFieldNavigator {
// MARK: - Toolbar & UIBarButtonItems
...
// MARK: - Properties
...
// MARK: - Initialization
init(with textFields: [UITextField]) {
self.textFields = textFields
subscribeToNotifications()
setupToolbar()
}
deinit {
unsubscribeToNotifications()
}
}
Paso 4: Configurar la navegación
¿Recuerdas los métodos que dejaste vacíos? Es hora de implementarlos.
Empezarás por el más sencillo: el didTapDone()
. Cuando el usuario pulsa este botón, simplemente quieres que se oculte el teclado. Para ello, tienes que hacer uso de resignFirstResponder()
:
@objc private func didTapDone() {
activeTextField?.resignFirstResponder()
}
En los métodos didTapPreviousTextField()
y didTapNextTextField()
tienes que hacer que el anterior o el siguiente TextField tengan el foco, respectivamente. Ten en cuenta que no se puede navegar al TextField previo si el que está en foco es el primero, o al TextField siguiente si es el último. Añade este código:
@objc private func didTapPreviousTextField() {
// 1
guard let activeTextField = activeTextField,
let index = textFields.firstIndex(of: activeTextField) else {
return
}
// 2
if !isFirstTextFieldActive {
moveFocus(to: textFields[index - 1])
}
}
@objc private func didTapNextTextField() {
// 1
guard let activeTextField = activeTextField,
let index = textFields.firstIndex(of: activeTextField) else {
return
}
// 2
if !isLastTextFieldActive {
moveFocus(to: textFields[index + 1])
}
}
private func moveFocus(to activeField: UITextField) {
// 3
activeField.becomeFirstResponder()
activeTextField = activeField
}
// 1:
Extraes el índice del actualactiveTextField
// 2:
Mueves el foco al TextField anterior o siguiente, comprobando que elactiveTextField
no seal el primero o el último, respectivamente.// 3:
Creas una función para mover el foco al TextField que se pasa por parámetro. También debes asignar dicho TextField como elactiveTextField
.
Paso 5: Asignar la toolbar
al activeTextField
Por último, debes asignar la toolbar
del TextFieldNavigator
como inputAccessoryView
del activeTextField
. Esto puedes hacerlo añadiendo un didSet
a esta propiedad:
class TextFieldNavigator {
// MARK: - Toolbar & UIBarButtonItems
...
// MARK: - Properties
private var textFields: [UITextField]
private weak var activeTextField: UITextField? {
didSet {
activeTextField?.inputAccessoryView = toolbar
}
}
// MARK: - Initialization
...
}
De esta manera, cada vez que se asigne un nuevo TextField como el activeTextField
se añadirá la toolbar
al teclado.
Utilizar el TextFieldNavigator
Imagina que tienes un FormViewController
con tres campos de texto. Hacer uso del navegador es tan sencillo como crear una instancia de TextFieldNavigator
y asignarle los TextFields.
class ViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var phoneNumberTextField: UITextField!
private var textFieldNavigator: TextFieldNavigator!
override func viewDidLoad() {
super.viewDidLoad()
textFieldNavigator = TextFieldNavigator(with: [
nameTextField,
mailTextField,
phoneNumberTextField
])
}
}
Constraint conflicts: En iOS 13, han aparecido unos conflictos en los constraints de la Toolbar. Puede que se trate de un bug en UIKit en iOS 13. Más información aquí.
Únete a la newsletter para no perderte ningún nuevo tutorial: recibe un email cuando se publiquen nuevos artículos o vídeos y no pierdas la oportunidad de seguir aprendiendo.
Conclusión
Crear formularios que sean tengan buena UX en iOS es un reto complicado. Si el equipo de UI/UX de Apple decidió añadir un UIToolbar
al teclado de todos los formularios que se abren en Safari, creo que es buena idea copiarles y hacerlo también en nuestras aplicaciones.
Como puedes comprobar, no es tan complicado crear un TextFieldNavigator
como el de Safari, y utilizarlo es todavía más fácil. Basta con crearte una instancia del navegador y pasarle los UITextField
.
Si tienes alguna duda, escríbeme aquí abajo un comentario o contáctame en mi Twitter. Por último, si te ha resultado útil y te ha gustado el artículo, ¿podrías compartirlo en tus redes?
Un abrazo!