Vacío Perfecto

Un poco acerca de nada

Interface Builder: Auto layout, UIScrollViews y Rotación

| Comments

Este es un artículo técnico, así que si venías buscando un artículo de divulgación, un comentario jocoso o una curiosidad, ya puedes dejar de leer y te ahorras un mal trago.

Auto Layout lleva ya algunos años con nostros y Apple se ha ido encargando de meternos hasta en la sopa las bondades de utilizar dicha tecnología. Sin embargo, entre la comunidad de desarrolladores, la versión de Interface Builder incluida en Xcode 4.x nos ha predispuesto a ser recelosos con esta tecnología. Por suerte con Xcode 5 el soporte de Auto Layout en Interface Builder es tan bueno que ahora no hay ninguna excusa para no utilizar esta tecnología. Bueno queda una única excusa. A veces Auto Layout es dificil de entender. Uno de esos casos es cuando juntamos Auto Layout, UIScrollView y rotaciones de dispositivo.

Un eje, dos condiciones

En la documentación de Auto Layout se recuerda constantemente que son necesarias dos condiciones por cada eje, para cada vista, a fin de definir un layout completamente. Veremos, a modo de preámbulo un problema muy sencillo que nos demostrará como la elección de esas dos condiciones no es trivial puesto que algunas de ellas tienen efectos colaterales.

Queremos colocar un rectángulo rojo centrado en un UIViewController. Super sencillo!, en IB arrastramos una UIView a la que le ponemos el fondo rojo hasta el centro de la view principal del UIViewController y añadimos dos condiciones del tipo centrar en vertical y centrar en horizontal respecto a la superview.

Como vemos las guias de Auto Layout aparecen en naranja indicando que hay algún problema con nuestro layout. Luego veremos como IB nos da información adicional que nos puede ser de mucha utilidad a la hora de resolver estos problemas, pero de momento, en este caso tan sencillo sabemos como resolverlo: solo hemos añadido una condición por cada eje (centrado en X y centrado en Y), así que nos hace falta añadir otra condición en cada eje.

La primera alternativa que se nos ocurre es fijar las dimensiones del rectangulo rojo. Para ello solo tenemos que añadir una condición fijando cada una de esas dimensiones

Y por supuesto funciona: ya tenemos el rectángulo rojo centrado en nuestro controlador (recordad, estamos en iOS7, las barras también forman parte de nuestro controlador). Inlcuso cuando rotamos el dispositivo, el rectángulo se mantiene centrado y con dimensiones constantes.

Ahora vamos a probar a cambiar una de las condiciones. Supongamos que en el eje X, en lugar de fijar la anchura de la view, fijamos la distancia del leading edge a la superview y mantenemos fija la altura:

En formato vertical el aspecto es exactamente el mismo, pero ¿que ocurre cuando rotamos el dispositivo? En esas condiciones, la pantalla ha pasado de 320 puntos de anchura a 568 (o 480) puntos, así que cuando Auto Layout intenta cumplir simultaneamente las dos condiciones horizontales, distancia del leading edge a la superview y posición del centro, el único modo de hacerlo es alterando la anchura de la view, haciéndola crecer:

Como vemos la vista roja mantiene su altura, pero su anchura ha crecido para poder estar centrada en la superview y al mismo tiempo a 60 puntos de borde izquierdo. Este efecto colateral nos será de mucha utilidad en el futuro, así que recordadlo.

Longcat

Primero algo evidente, pero que muchas veces olvidamos: Interface Builder está ahí para ayudarnos, no para hacernos la vida más complicada. Uno de los momentos en los que resulta más útil es cuando tenemos que crear el layout de una vista muy grande, de esas que tenemos que embeber en un scrollview para poder verla toda. En concreto nos vamos a centrar en el típico formulario que tiene una altura mucho mayor que nuestro dispositivo y al que por tanto tenemos que dotar de scroll vertical.

Nada más abrir el View Controller en IB mira la cuarta pestaña

¿Ves el tamaño? Pues selecciona freeform. De ese modo podrás cambiar la altura de la view y verla toda a la vez:

El resultado es que ahora tenemos una longview donde podemos disponer todos los elementos de nuestro interface con comodidad

Vistas dentro de vistas dentro de vistas dentro de

Pero antes de ponernos a crear nuestro magnífico formulario, es necesario que nos preparemos la infraestructura necesaria para que el usuario final pueda manejar esta vista. La solución ya la conocéis: meter un UIScrollView ocupando todo el ViewController y sobre el añadir los controles oportunos. Sin embargo, cuando trabajamos con Auto Layout el modo más sencillo es añadir al UIScrollView una UIView que nos sirva de contenedor de todos los controles. Algo así:

Y ahora empezamos con el Auto Layout. Lo primero es “atar” el UIScrollView a la view principal del UIViewController. Eso es sencillo, cuatro condiciones cada una atando un extremo del UIScrollView a su superview

Como podéis ver las condiciones aparecen a la misma “profundidad” en el inspector que la UIScrollView, indicando que son condiciones entre esa view y su superview. Es importante fijarse en estas cosas cuando trabajas con Auto Layout, te ayudan a saber donde estás y sobre todo, porqué

Ahora viene la complejidad. Dentro del UIScrollView tenemos un UIView. Cuando atemos el UIView al UIScrollView mediante restricciones de Auto Layout, lo que estamos haciendo realmente es fijar el contentSize del UIScrollView. Como lo oyes. Así de extraño. Es tan extraño que incluso Apple ha tenido que sacar una nota técnica explicando el porqué de este comportamiento. Bueno, el tamaño de nuestro contenido está claro: el ancho de la vista y el alto de 800 puntos que necesitamos para mostrar todo nuestro contenido, así que atamos el UIView contenedor con la UIScrollView por los cuatro bordes y fijamos la altura de la vista en 800 puntos:

De nuevo, si revisamos la estructura de la escena veremos la condición explicita de la Container View (su altura), las condiciones que la relacionan con el scroller y las relaciones entre este y la view principal del controlador

Sin embargo, en esa misma vista podemos ver que hay una flecha roja en la esquina superior derecha. Eso indica que IB tiene alguna objeccion respecto a nuestro layout. Esta es una gran ventaja de utilizar Auto Layout con Interface Builder en Xcode 5, el debug en tiempo de creación de nuestro interface. Pasamos a ver que problemas nos plantea, para ello hacemos click sobre esa flechita roja y vemos los siguientes warnings

Empezamos con el segundo problema: el content view tiene una anchura de 320 puntos mientras que por las restricciones que le hemos puesto deberia de ser 0. ¿Como? ¿Por qué cero? Pues porque lo hemos atado al contentSize de la UIScrollView que por defecto es CGSizeZero, así que esa condición está intentando colapsar la UIView.

El primer problema que aparece lo tiene la UIScrollView: no tiene suficientes condiciones para saber cual es su contentSize en horizontal. ¿Como que no? Si le hemos dicho que sea igual que la content view… la cual no tiene una anchura definida, sino que depende el UIScrollView contentSize… ¿empiezas a ver el problema?

Bueno, supongamos que nos da igual, o lo que suele pasar cuando estás empezando con autolayout, que no te has dado cuenta de estas alertas y has ignorado que todas las guias de layout son ahora naranjas. Sí, lo sé, es mucho ignorar, pero todos tenemos esos días. Así que como os decía, os liais la manta a la cabeza y empezáis a meter otros controles dentro de la content view para montar vuestro inteface. Ahí es cuando empiezan a ocurrir cosas extrañas:

Como veis, pese a que todas las condiciones del UITextField son azules, la frame que nos propone no coincide con la que hemos dibujado ¿Cómo? ¿Cómo pueden estar bien las distancias a los bordes de la superview, pero no el tamaño del control? ¿Se ha vuelto loco Auto Layout? ¿Está fallando IB? Pues no, ni lo uno ni lo otro, simplemente lo que no está definido es el tamaño de la superview de este control (la content view) y Auto Layout está haciendo lo mejor que puede para resolver este entuerto. Por ejemplo, si seleccionamos la content view para ver su frame, veremos algo curioso:

¿Veis el borde discontinuo? Ha colapsado todo lo que ha podido hasta encontrarse con la etiqueta, que como tiene tamaño propio y posición fija detiene su compresión. Es esa frame comprimida, la que cuando le aplicas la distancia al borde derecho hace que el UITextField se comprima tanto y se quede con la frame que veíamos antes.

Así que todos estos fantasmas provienen de los dos warnings que hemos ignorado antes.

Y claro, podemos seguir adelante y probar la aplicación. A veces funcionará. A veces colapsará la frame y en cuanto giremos el dispositivo no entederemos que demonios está pasando:

Anclando el contentSize

Ya tenemos clara nuestra misión: tenemos que definir completamente el contentSize de la UIScrollView. Bueno esa es fácil, solo tenemos que fijar la anchura del Content View:

Bingo! han desaparecido todos los warnings, nuestras frame y conditions vuelven a ser todas azules!. Hace un día magnífico y ya se manejar autolayout!.

Un momento ¿que has hecho?

¿yo? nada, bueno, he girado el móvil.

Claro, 320 puntos solo nos sirve como anchura de nuestro dispositivo en una determinada orientación. Bueno, siempre se puede implementar layoutSubviews y alli calcular la anchura y…. no, tranquilos no vamos a escribir ni una sola linea de código en este artículo ¿os acordais del preámbulo?

En efecto, vamos a fijar el centro en X del content view al centro en X del scroll view, el cual a su vez está anclado indirectamente al centro en X de la root view de nuestro UIViewController:

Cuando ahora giramos el dispositivo, el UIViewController cambia la frame de su view, lo cual cambia el tamaño del UIScrollView, lo cual a su vez desplaza su centro en X. Este desplazamiento del centro se transmite al UIView contenedor que también tiene su borde izquierdo fijado al del UIScrollView (a cero) y para poder mantener esas dos condiciones, Auto Layout modifica la anchura del content view, lo cual a su vez provoca el cambio en el contenSize del UIScrollView y así, finalmente tenemos un UIScrollView completamente funcional, respondiendo a rotaciones y todo ello creado en IB sin una sola linea de código.

¿No es maravilloso cuando entiendes que está pasando y como funcionan las cosas por dentro? Eso sí, si le pegas un vistazo a la estructura final del controller que hemos creado con todas las condiciones internas de los controles que he puesto en el Content View, la cosa asusta un poco

Lo que hay que hacer es acordarse de que cada restricción aparece al nivel afectado. Así, todas las condiciones que están al mismo nivel que la content view afectan unicamente a su contenido (las posiciones de los controles) o a ella misma (su altura).

Aunque el ejemplo es muy sencillo, he subido a GitHub los fuentes por si quieres explorar el resultado sin tener que reconstruir todo el ejemplo.

Comments