Tutorial desarrollo iOS – Diseño de interfaz adaptativo con Autolayout: UIStackView y Vary for Traits

En el desarrollo de una APP para iOS, no son pocas las ocasiones en las que una misma pantalla tiene distinta composición en orientación vertical (portrait) y horizontal (landscape). Hoy te contamos cómo puedes desarrollar una interface adaptativa para tus apps.

Durante años ha habido distintas formas de afrontar este problema con Xcode. Desde la re-ubicación de componentes mediante código, a la creación de distintos layouts para cada versión.

El Autolayout aportó una nueva manera de gestionar esta casuística. El “Size-Class-Specific Layout” consistía en una opción en cada xib que permitía definir comportamientos específicos según distintos tamaños de pantalla. Siendo útil, no sólo para cambios de orientación, sino también para distintos tamaños de pantalla o cambios en el contenedor del layout (como pueda ser el master-view-controller de una SplitView en iPad).

Esta herramienta permitía establecer distintos comportamientos para distintos tamaños y configuraciones de pantalla sin necesidad de recurrir a cambios por código o a tener más de un fichero xib.

En Xcode 8 fue sustituido por la opción Vary for traits, que tiene un comportamiento muy similar aunque puede llegar a ser algo confuso cuando se usa por primera vez.

Vamos a navegar un poco por las opciones que ofrece esta característica y a realizar una pantalla de ejemplo usando constraints en AutoLayout, que nos permita reusar un mismo diseño aplicando únicamente cambios en la composición de los elementos de la interfaz.

El ejemplo será muy simple y en él usaremos AutoLayout con distinta visualización en portrait y landscape (vertical y horizontal).

Creación del ViewController

En primer lugar, creamos un nuevo proyecto de Xcode. En el wizard seleccionamos Single View Application. El proyecto creado consta de su AppDelegate y un ViewController, así como el Main.storyboard.

En este ejemplo vamos a sustituir el Main.storyboard por un fichero xib que contenga únicamente la pantalla que queremos diseñar.

Para ello, creamos un fichero xib llamado ViewController.xib y añadimos en la clase ViewController.m el siguiente código:

-(id)init {
ViewController *controller = [super initWithNibName:NSStringFromClass([self class]) bundle:nil];

return controller;

}

En el xib es importante establecer en el campo Custom Class del Identity Inspector el nombre de nuestra clase:

Y arrastrar en el Connection Inspector el campo View a la main view del controller.

Hasta aquí ya tendríamos creado el controller de nuestra vista, y vinculado con su fichero de interface builder. Sólo quedaría abrirlo desde el AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

// Override point for customization after application launch.

ViewController *viewController = [[ViewController alloc] init];

UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];

self.window.rootViewController = navigationController;

return YES;

AutoLayout

Ahora vamos añadir los campos a nuestro controller. Vamos a montar una pantalla de Login que contenga un logo, un par de cajas de texto para añadir el usuario y la contraseña, y, por último, un pie de página.

Para ello vamos a usar AutoLayout y, en el siguiente apartado, tendremos especial cuidado de que no sea necesario hacer scroll cuando estemos en un iPhone en posición Landscape, ya que cambiaremos de forma dinámica la ubicación de los componentes gracias al Vary for traits.

Hay distintas formas de crear esta pantalla, probablemente, alguna de ellas más fáciles e intuitivas, pero vamos a optar por usar el componente UIStackView introducido en iOS 9, ya que realiza algunas funciones automáticamente y nos va a resultar más cómodo de utilizar a la larga.

En primer lugar, vamos a añadir el pie que querremos mantener en la zona de abajo de la pantalla tanto en portrait como el landscape, por lo que nos limitaremos a añadir un label y anclarlo en el bottom de la view y a los lados. La altura la dejaremos dinámico por si en algún caso pudiese tener texto que ocupase más de una línea, y centraremos el texto en con campo Alignment del Label.

Ahora, añadimos a la View un UIStackView vertical que contendrá 2 elementos: el logo por un lado, y los campos de usuario y contraseña por otro. Y lo anclaremos a los 4 lados de la view.

Ahora vamos a añadir 2 UIView que harán de contenedoras, ya que un UIStackView no nos permite establecer un margen independiente para cada elemento. De hecho, es un componente que tampoco permite añadir un background o un borde, ya que no es una UIView propiamente dicha, sino una extensión cuyo único propósito es posicionar las vistas.

Al añadir ambas UIView al UIStackView Xcode se quejará de que nos faltan constraints. Y es cierto, ya que como no se sabe cuánto ocupará cada vista, no es posible asignar el espacio restante a la otra vista. Por la composición de la pantalla, vamos a querer que el logo ocupe un lugar dominante en la parte superior de la misma, dejando las cajas de texto para la parte inferior. Por lo que vamos a optar por asignar mitad y mitad a cada vista mediante el constraint Equal Heights.

Para mayor claridad, la vista que contendrá al logo será amarilla, y la vista que contendrá la caja de login será azul. Deberíamos tener algo como esto:

Ya sólo nos queda añadir el logo y las cajas de texto. Empecemos por el primero, vamos a ubicar la vista dentro de la view amarilla, y le vamos a asignar como únicas constraints que esté centrado vertical y horizontalmente.

Ya tenemos nuestro “logo” (por comodidad he usado un UILabel pero a efectos prácticos es lo mismo). Ahora, para la zona del acceso, usaremos otra UIStackView vertical en el que ubicamos 3 vistas  con background transparente (clear color). Dos de ellas con un con un icono y una caja de texto, y la tercera con el botón de Acceder.
Añadimos nuestro nuevo UIStackView vertical a la UIView azul, y dentro añadiremos las 3 UIView.

Ahora mismo, Xcode no sabe ni cuánto ocupará cada vista de ancho, de alto, o dónde se ubicará la propia UIStackView dentro de lo que sería nuestra UIView azul. Por lo que es normal que salgan avisos de missing constraints. Vamos a obviarlos por ahora.

A continuación, añadiremos los iconos de usuario y contraseña  en las dos primeras vistas, junto con un UITextView. Este será el único componente en el que fijaremos el width. Ya que no queremos que sea dinámico y se ajuste al ancho de la pantalla (podría llegar a ocupar demasiado espacio y no quedaría muy atractivo visualmente).

Los iconos los hemos añadido a la carpeta Assets.xcassets con el nombre pass_ic y user_ic. Respecto a las constraints, el icono va a estar anclado a la izquierda, centrado verticalmente, y va a tener un tamaño fijo de 30×30 (ya que nuestros iconos son de esas dimensiones aproximadas).

La caja de texto va a tener una anchura de, por ejemplo, 200, y va a estar anclada a la derecha, arriba y abajo de la vista padre, y al icono por el constraint izquierdo. También va a tener una altura de 44, para que sea suficientemente cómoda.

Teniendo ya definida la altura de la primera UIView (que será la altura del UITextView, ya que tiene altura fija y está anclada tanto arriba como abajo), y teniendo la anchura (que será la del UITextView + la del UIImageView +  la separación entre ellas, ya que todos estos valores están fijados). El UIStackView contenedor puede inferir el tamaño que va a necesitar en anchura y en altura una vez terminemos las 3 vistas.

Vamos a repetir el proceso para la vista del password y para el botón de acceso, que anclaremos a la vista padre, y centramos este UIStackView en la vista azul.

Debería quedar algo como esto:

Las cajas de texto están muy juntas entre ellas. Una opción más elegante de separarlas, en vez de modificar las contraints del UITextView, es añadirle separación al UIStackView. Esto lo podemos hacer desde el Identity Inspector modificando el campo Spacing.

Vary for traits

Sin embargo, nuestra vista en landscape no se visualiza exactamente como nos gustaría. El pie no llega a verse y se desperdicia mucho espacio a los lados, mientras que toda la información importante se ubica en ⅓ de la pantalla.

Para solucionar esto, vamos a usar la opción de Vary for traits, a la que se puede acceder desde la parte inferior del InterfaceBuilder, pulsando en View as:
Cuando lo pulsamos, podemos decidir para qué casos queremos añadir constraints concretas. Si serán constraints que se aplicarán sólo cuando cambie la anchura, o cuando cambie la altura, o cuando cambien altura y anchura. En la parte inferior nos aparecerán gráficamente los casos para los que aplicará este cambio.

Es importante fijarse en el (wC hC). Esto nos da una pista de qué casos aplica. w = anchura, h = altura, y el valor de la derecha podrá tomar R=Regular, C=Compat o A=Any.

Varios ejemplos serían:

  • Un iPhone en portrait será wC hR
  • Un iPad será wR hR
  • Un iPhone 7 o SE en landscape será wC hC
  • Un iPhone 7 Plus en landscape será wR hC

Básicamente Regular se refiere a tamaños grandes, y Compact a tamaños pequeños. Una constraint que sólo aplique a los casos en los que nuestra vista se deforma, es una constraint que debe añadirse para los casos hC, ya que un iPad en landscape no queremos cambiarlo.

Por lo que en nuestro caso, seleccionaremos el iPhone 7 en landscape (wC hC), por ejemplo, y pulsaremos Vary for Traits para height y width, mostrándonos todas las variantes posibles que tienen ese wC hC.

En este punto, todo cambio que hagamos sobre los layouts se aplicará únicamente a estas variantes. Vamos pues a cambiar lo justo, que no es mucho gracias a los UIStackView y al parámetro orientación, y veremos cómo se comporta.

En primer lugar, vamos a añadir una constraint que indique que tanto la vista azul como la amarilla tengan la misma anchura. Esto no era importante con la orientación vertical del UIStackView, pero en el caso landscape es importante añadirlo:

Nótese cómo a partir de ahora, cuando pulsamos en Done varying, las constraints que hayamos añadido en modo Vary for traits aparecerán en gris cuando estemos visualizando un caso en el que esa constraint no aplica.

La constraint de Equal Widths sólo se usa cuando hC wC, es decir, en iPhone landscape.

Ahora vamos a volver a usar Vary for traits pero no va a ser en una constraint, sino en el Identity Inspector. Vamos a establecer qué queremos que nuestro UIStackView principal sea horizontal cuando estamos en el caso wChC.

Para ello, desde la propia pestaña del Identity Inspector, con el UIStackView seleccionado, pulsamos en + junto al atributo Axis.

Nos aparecerá un pequeño menú en el que indicaremos para qué casos vamos a querer modificar el atributo Axis.

Añadimos el nuevo atributo axis y lo modificamos para que sea horizontal en el caso wChC

El resultado es el que queríamos obtener. Un nuevo diseño que aproveche mejor el espacio cuando estoy en un iPhone en Landscape.

Y por último: el keyboard

A estas alturas Xcode no ofrece ninguna herramienta (como sí hacen las herramientas de  Android) para evitar que el teclado oculte una caja de texto.

Existen miles de formas de hacerlo, pero ninguna es lo suficientemente limpia. La mayoría de ellas son manuales e implican ensuciar el código para un comportamiento que debería venir en el propio SDK.

En el caso que nos ocupa, vamos a usar la librería IQKeyboardManager para no dedicar código y tiempo a algo que ya está hecho, y probablemente, bastante bien.

Crearemos el .framework usando Carthage (también se puede usar CocoaPods). Lo añadimos al proyecto y añadimos la siguiente línea al didFinishLaunchingWithOptions del AppDelegate.m

IQKeyboardManager.sharedManager.enable=true;

En resumen

Con todo estos pasos, ya tendríamos nuestra pantalla de login perfectamente funcional. Sólo nos quedaría terminar de añadir la funcionalidad del botón y los colores, diseños, etc.

AutoLayout puede llegar a ser tu amigo si se trata con cierto cuidado. En las últimas versiones de iOS, con las diversas mejoras al sistema de constraints y sobretodo la introducción del UIStackView, realizar diseños de interfaz usando código es algo que puede evitarse en la mayoría de ocasiones, dando como resultado un proyecto mucho más limpio y organizado.

Desgraciadamente, aún dista de ser perfecto y el Interface Builder sigue mostrando algunas carencias y bugs que hacen acto de presencia en composiciones más complejas, así como grandes problemas en el uso de sistemas de control de versiones. Esperemos que Apple lo siga mejorando en sucesivas iteraciones.