Este es un problema muy común cuando comenzamos a desarrollar formularios para iOS. Nos liamos a colocar UITextFields uno encima de otro y cuando compilamos y probamos en nuestro dispositivo (sobretodo en iPhone, que la pantalla es más limitada que en iPad) ¡sorpresa!

KeyHide

Buscando por Internet se pueden encontrar distintos tipos de soluciones al problema, pero ninguna de las que probé acababa funcionando como yo quería.

Así que después de varias pruebas sin éxito decidí pararme a pensar y desarrollar mi propia solución, ayudándome de las anteriores aproximaciones que había probado de otros desarrolladores.

Unos buenos cimientos

En primer lugar, tenemos que establecer una estructura básica que prácticamente todo UIView con campos de texto debería de cumplir, es la siguiente:

View (UIView Controller) -> Scroll View (UIScrollView) -> Content View (UIView)

scroll

Antes de empezar a poner objetos a nuestra vista deberemos añadir un UIScrollView dentro del View por defecto:

content01

 

Y dentro de este pondremos otro UIView al que llamaremos ‘Content View’. Por supuesto ambos elementos (el Scroll View y el Content View) los anclaremos a los 4 márgenes (top, bottom, left, right) mediante la herramienta Pin con 0 unidades de espacio.

content02

 

Ahora tenemos que establecer la altura y anchura del ‘Content View’ para evitar el error de contenido ambiguo. Le asignaremos la misma altura y anchura que el View padre del Scroll View.

content03

Recuerda que podemos hacer la misma operación en un solo paso, manteniendo apretada la tecla shift y seleccionando ‘Equal Widths’ y ‘Equal Heights’ de una vez.

Por último, establecemos la prioridad del constraint de la altura en ‘Low(250)’

content04

 

Con esta estructura creada ya podemos empezar a colocar todos nuestros objetos y vistas dentro del ‘Content View’

El formulario

La idea es la siguiente, cada vez que el teclado se muestre en pantalla comprobaremos que UITextField tiene el foco y calcularemos si este va a quedar oculto por el teclado. En caso afirmativo moveremos el Scroll View para dejar el UITextField justo encima del teclado y a la vista del usuario.

Para ello primero deberemos de realizar lo siguientes pasos:

  1. Saber cuando se está mostrando el teclado
  2. Averiguamos la altura de la pantalla
  3. Calculamos la coordenada ‘Y’ máxima del UITextField
  4. Calculamos la coordenada ‘Y’ máxima que se va a ver en pantalla, en función del tamaño del teclado y otros elementos que puedan haber en pantalla (TabBar, NavBar, StatusBar…)
  5. Si la ‘Y’ max del UITextField es mayor que la ‘Y’ max que va a verse en pantalla entonces deberemos mover el Scroll View para dejar el UITextField a la vista (Ya te adelanto que jugaremos animando los constraints)

62871401

Para hacerlo sencillo vamos a hacer un diseño de formulario que solo tendrá dos UITextFields. Uno lo colocaremos arriba de modo que nunca quede oculto por el teclado y el otro lo colocaremos muy abajo de manera que siempre quede oculto.

content05

Y como no, nuestras queridas constraints (restricciones) para que todo quede perfecto:

content06

Fíjate como además de establecer los márgenes para cada UITextField también establezco una restricción de espacio vertical entre ambos campos de texto. Esto es para que sea del tamaño que sea la pantalla ambos campos de textos siempre tengan la misma separación con el fin de apreciarse mejor el efecto del Scroll View al final del ejemplo.

ViewController: El código

Pasemos al ViewController.swift de la vista. Vamos a hacer uso de la clase NSNotificationCenter para que nos avise cada vez que el teclado se muestre y se oculte. Para ello añadimos las siguientes lineas dentro de nuestro método viewDidLoad():

1
2
3
4
5
6
7
8
9
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.

// Keyboard notifications
var center: NSNotificationCenter = NSNotificationCenter.defaultCenter()
center.addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
center.addObserver(self, selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
}

Es importante que dejemos ya listo también el método para desuscribirse de estos eventos. Lo haremos en el viewWillDisappear():

1
2
3
4
override func viewWillDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}

A continuación implementamos los métodos que nos van a permitir saber cuando se muestra y cuando oculta el teclado:

1
2
3
4
5
6
7
8
9
10
func keyboardWillShow(notification: NSNotification) {
var info:NSDictionary = notification.userInfo!
var keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()

println("El teclado se va a mostrar y tiene una altura de: \(keyboardSize.height)")
}

func keyboardWillHide(notification: NSNotification) {
println("El teclado se va a ocultar")
}

Personalmente me gusta que el teclado se oculte automaticamente cuando tocamos cualquier otra cosa que no sea el UITextField que estamos editando u otro UITextField. Para ello vamos a crear un Gesture Recognizer para saber cuando hacemos ‘Tap’ en el Scroll View y entonces ocultar el teclado.

Definimos la siguiente variable en nuestra clase:

1
2
// GestureRecognizer to catch the tap on ScrollView
var scrollGestureRecognizer: UITapGestureRecognizer!

y la inicializamos en nuestro viewDidLoad(). Ojo: Para poder asignarle el Gesture Recognizer al Scroll View previamente tienes que crear un IBOutlet y referenciarlo en tu ViewController (no voy a entrar en esos detalles):

1
2
3
4
// Inside viewDidLoad()
// Get the Tap gesture on ScrollView to hide the keyboard
scrollGestureRecognizer = UITapGestureRecognizer(target: self, action: "hideKeyBoard")
scrollView.addGestureRecognizer(scrollGestureRecognizer)

E implementamos el método al cual estamos llamando cada vez que el user hace ‘Tap’ en el Scroll View:

1
2
3
4
func hideKeyBoard(){
// Hide the keyboard
view.endEditing(true)
}

Ya tenemos el teclado apareciendo cuando editamos un UITextField, el de arriba se ve perfecto pero el de abajo…ups queda oculto por el teclado.

content07

 

Como ya he comentado lineas arriba, vamos a solucionar este problema jugando con los constraint, una solución que me parece bastante limpia, aunque seguro que hay muchas más y mejores.

Para ello vamos a necesitar modificar la constante del bottom constraint del último UITextField.

Nos crearnos un IBOutlet de este constraint:

content08

Vamos a utilizar también una variable en la clase para almacenar el tamaño del teclado:

1
var keyboardHeight:CGFloat!

Añadimos en el método keyboardWillShow() una linea para almacenar la altura del teclado que va a mostrarse. Haremos uso de esta variable en otro método:

1
2
3
4
5
6
7
func keyboardWillShow(notification: NSNotification) {
var info:NSDictionary = notification.userInfo!
var keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()

// Store the keyboard height
keyboardHeight = keyboardSize.height
}

Cuando el teclado se muestre quiero saber que UITextField tiene el foco, para ello necesito guardar en un array los dos UITextFields. Creamos un array de UITextFields en la clase:

1
var textFields: [UITextField]!

En el método viewDidLoad() inicializamos el array con los dos UITextFields. Para ello deberemos de haber creado antes dos IBOutlets para referenciar en nuestro ViewController los dos UITextFields:

1
textFields = [firstTextField, secondTextField]

A continuación, el método más importante, el que va a hacer todos los cálculos para comprobar si el UITextField queda oculto por el teclado o no, y el que va a modificar el content offset del Scroll View para desplazar el UITextField hacia arriba:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func setScrollViewPosition(){
    // Modificamos el valor de la constante del constraint inferior, le damos la altura del teclado más 20 de marge. De este modo estamos agrandando el Scroll View lo suficiente para poder hacer scroll hacia arriba y trasladar el UITextField hasta que quede a la vista del usuario. Ejecutamos el cambio en el constraint con la función layoutIfNeeded().
    bottomConstraint.constant = keyboardHeight + 20
    self.view.layoutIfNeeded()

    // Calculamos la altura de la pantalla
    let screenSize: CGRect = UIScreen.mainScreen().bounds
    let screenHeight: CGFloat = screenSize.height

    // Recorremos el array de textFields en busca de quien tiene el foco
    for textField in textFields {
        if textField.isFirstResponder() {
            // Guardamos la posición 'Y' del UITextField
            let yPositionField = textField.frame.origin.y
            // Guardamos la altura del UITextField
            let heightField = textField.frame.size.height
            // Calculamos la 'Y' máxima del UITextField
            let yPositionMaxField = yPositionField + heightField
            // Calculamos la 'Y' máxima del View que no queda oculta por el teclado
            let Ymax = screenHeight - keyboardHeight
            // Comprobamos si nuestra 'Y' máxima del UITextField es superior a la Ymax
            if Ymax < yPositionMaxField {
                // Comprobar si la 'Ymax' el UITextField es más grande que el tamaño de la pantalla
                    if yPositionMaxField > screenHeight {
                        let diff = yPositionMaxField - screenHeight
                        println("El UITextField se sale por debajo \(diff) unidades")
                        // Hay que añadir la distancia a la que está por debajo el UITextField ya que se sale del screen height
                        scrollView.setContentOffset(CGPointMake(0, self.keyboardHeight + diff), animated: true)
                    }else{
                        // El UITextField queda oculto por el teclado, entonces movemos el Scroll View
                        scrollView.setContentOffset(CGPointMake(0, self.keyboardHeight - 20), animated: true)

                    }
                }else{println("NO MUEVO EL SCROLL")}
            }
        }
    }

Llamamos al nuevo método desde la función keyboardWillShow():

1
2
3
4
5
6
7
func keyboardWillShow(notification: NSNotification) {
var info:NSDictionary = notification.userInfo!
var keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()
keyboardHeight = keyboardSize.height

setScrollViewPosition()
}

Finalmente volvemos a dejar todo como estaba cuando el teclado desaparece:

1
2
3
4
func keyboardWillHide(notification: NSNotification) {
bottomConstraint.constant = 40
self.view.layoutIfNeeded()
}

Y este es el resultado final:

content09

 

Como mejorarlo

Ahora ya no se nos quedan ocultos tras el teclado los campos de texto, pero sería fantástico si además después de confirmar el primer UITextField nos llevara directamente al siguiente campo de texto ajustando también el scroll view de manera automática, pero eso… son deberes para casa.

Unas pistas…

1
2
optional func textFieldShouldReturn(_ textField: UITextField) -> Bool
optional func textFieldDidBeginEditing(_ textField: UITextField)

Además, habrá que ir con cuidado si tenemos un NavBar, TabBar o StatusBar para tenerlo en cuenta a la hora de calcular el tamaño visible de la pantalla. Es cuestión de ir cogiendo los tamaños de cada componente adicional que se esté mostrando y añadirlo a la suma.

kidsuccess

Puedes descargar el código completo de este tutorial desde mi cuenta de GitHub.</>

(Implementado usando Xcode 6.4 y Swift 1.2)

Editado 01: 26-08-2015 – Solventado bug. Cuando la posición Y max del UITextField que estamos editando era mayor que la altura de la pantalla el scroll no se posicionaba correctamente.

About Pablo Marcos

Soy ingeniero informático y apasionado de la programación y la tecnología. Actualmente viviendo en Alemania y trabajando como desarrollador interactivo en Stoll Von Gáti GmbH. Mi twitter @pablo_marcos