Skip to content

¿Cómo crear un TextFieldNavigator en Swift?

2 marzo, 2020

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 UITextFields 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 tipo UIBarButtonItem.
  • Crear un init y llamar en él al método setupToolbar(), 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 la UIToolbar
  • // 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 un spaceButton entre las flechas y el botón "Done". Por último, llamas a sizeToFit() 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 de UITextFields 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 el init de manera que acepte un array de UITextFields 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ón UITextField.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 array textFields.
  • // 4: Asignas el TextField que se está editando como el activeTextField.
  • // 5: Deshabilitas el previousButton si el TextField activo es el primero, y el nextButton 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 actual activeTextField
  • // 2: Mueves el foco al TextField anterior o siguiente, comprobando que el activeTextField 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 el activeTextField.

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í.

TextFieldNavigator

Ú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!


¿Me ayudas a compartir en redes sociales?