Cómo crear hermosos diseños elásticos en iOS usando Auto Layout y SnapKit

Mira la imagen a continuación. Este es un efecto genial.

Y es realmente fácil de construir en iOS usando el diseño automático. Quería escribir sobre esto porque el efecto es muy simple. Y Auto Layout hace que su implementación sea tan elegante que creo que debería saberlo.

Si desea seguirlo, puede clonar el proyecto de demostración en nuestro punto de partida e implementar el efecto a medida que lo lea. Necesitará Xcode 9 ya que vamos con todo incluido en iOS 11 para este ejemplo.

git clone https://github.com/TwoLivesLeft/StretchyLayout.git
cd StretchyLayout
git checkout Paso-1

Así es como lo haremos:

  • Comience con la aplicación básica no elástica
  • Modifique la jerarquía de vistas para agregar las restricciones necesarias a
    hazlo elástico
  • Añadir esmalte a la aplicación

La aplicación no elástica

Aquí está la jerarquía de vistas para la versión básica de la aplicación. Puedes ver que tiene tres vistas principales. Existe el encabezado UIImageView, que es el contenedor del texto, y el UILabel largo que contiene nuestro contenido de texto. Las líneas rojas brillantes representan nuestras restricciones de diseño automático. También hay un UIScrollView y la vista raíz de nuestro controlador de vista.

Vamos a construir esto usando un marco de diseño automático llamado SnapKit. SnapKit es un marco simple de iOS que hace que la API de diseño automático de Apple sea sensata. Es muy fácil de usar y hace que la programación con Auto Layout sea realmente placentera.

La mayor parte del código vivirá en viewDidLoad de nuestra clase StretchyViewController. A continuación puede ver cómo se establecen las restricciones iniciales.

Nuestras opiniones se declaran como miembros privados:

private let scrollView = UIScrollView ()
privado let infoText = UILabel ()
privado let imageView = UIImageView ()

La vista de nuestro controlador de vista tiene una vista de desplazamiento como su primera subvista, seguida de las vistas de texto e imagen. También tiene una vista de respaldo que nos proporciona
El fondo rojo detrás del texto.

// Ancla los bordes de la vista de desplazamiento a
// la vista de nuestro controlador de vista
scrollView.snp.makeConstraints {
 hacer en
make.edges.equalTo (ver)
}
// Pin la parte superior de nuestra vista de imagen a la vista de desplazamiento
// fija la izquierda y la derecha a la vista del controlador de vista
// dale una restricción de relación de aspecto al restringir
// su altura a su ancho con un multiplicador
imageView.snp.makeConstraints {
 hacer en
make.top.equalTo (scrollView)
 make.left.right.equalTo (ver)
 make.height.equalTo (imageView.snp.width) .multipliedBy (0.7)
}
// Fije la vista de respaldo debajo de la vista de imagen y a la
// parte inferior de la vista de desplazamiento
textContainer.snp.makeConstraints {
 hacer en
make.top.equalTo (imageView.snp.bottom)
 make.left.right.equalTo (ver)
 make.bottom.equalTo (scrollView)
}
// Ancla los bordes del texto a la vista del contenedor de texto, esto
// obligará al contenedor de texto a crecer para abarcar el
// altura del texto
infoText.snp.makeConstraints {
 hacer en
make.edges.equalTo (textContainer) .inset (14)
}

Nota: Para obtener el código en este punto, haga git checkout Paso-1

Un breve aparte

Usamos SnapKit arriba. SnapKit es genial, así que aquí hay una introducción sobre cómo funciona.

Accede al objeto miembro snp en cualquier UIView.

Llama a makeConstraints, que acepta un cierre, y al cierre se le asigna un objeto ConstraintMaker (llamado make here).

Luego, utiliza el objeto de creación para fijar bordes o anclajes de una vista a cualquier otra vista, guía de diseño o constante.

myView.snp.makeConstraints {
 hacer en
make.edges.equalTo (ver)
}

Esto fijará los bordes de myView a los bordes de la vista.

Es legible y conciso. Use esto en lugar de la API de diseño automático predeterminada.

Haciéndolo elástico

Entonces, ¿cómo pasamos de esto (no elástico) a esto (elástico)?

Para el efecto de estiramiento, es importante que el diseño automático resuelva las restricciones
independientemente de si sus puntos de vista son hermanos u otros en la jerarquía.
Solo importa que tengan un ancestro común.

Pero aquí hay un elemento clave: las vistas dentro de las vistas de desplazamiento se pueden restringir a vistas fuera de las vistas de desplazamiento. Así es como haremos que esto funcione.

En el diagrama anterior, las líneas rojas brillantes representan nuestras restricciones. Observe cómo la parte superior de la vista de imagen ahora está fijada completamente hacia atrás, fuera del desplazamiento
vista: en la parte superior de la vista raíz. Pero su parte inferior está fijada en la parte inferior de la vista del contenedor de imágenes, que está dentro de la vista de desplazamiento. Esto significa que
cuando la vista de desplazamiento se desplaza, nuestra vista de imagen se estirará para satisfacer sus limitaciones.

Entonces, para nuestro primer paso, reemplazaremos nuestro UIImageView con una vista de contenedor vacía.

let imageContainer = UIView ()
imageContainer.backgroundColor = .darkGray
scrollView.addSubview (imageContainer)
imageContainer.snp.makeConstraints {
 hacer en
make.top.equalTo (scrollView)
 make.left.right.equalTo (ver)
 make.height.equalTo (imageContainer.snp.width) .multipliedBy (0.7)
}

Luego agregaremos nuestra vista de imagen como una subvista de nuestra vista de desplazamiento. Pero vamos a fijar su borde superior al borde superior de nuestra vista, no a la parte superior de nuestra vista de desplazamiento. El contenedor que acabamos de agregar está anclado al borde superior de nuestra vista de desplazamiento.

scrollView.addSubview (imageView)
imageView.snp.makeConstraints {
 hacer en
make.left.right.equalTo (imageContainer)
// ** ¡Estas son las líneas clave! ** **
 make.top.equalTo (ver)
 make.bottom.equalTo (imageContainer.snp.bottom)
}

Arriba puedes ver las líneas que hacen que esto funcione. Nuestra vista de contenedor de imágenes
se desplaza exactamente como se desplazó la aplicación original no elástica. Pero hemos agregado nuestra vista de imagen real sobre ese contenedor. Y hemos clavado su fondo
en la parte inferior del contenedor, mientras que su parte superior está anclada a la vista del controlador de vista.

Esto significa que cuando arrastra hacia abajo en la vista de desplazamiento, la parte superior de la imagen
"Se adhiere" a la parte superior de la pantalla y toda la imagen se vuelve más grande. Y
porque estamos usando imageView.contentMode = .scaleAspectFill, vamos a ver el contenido de la imagen escalar dentro de la vista de imagen a medida que nos desplazamos sobre la vista de desplazamiento.

Nota: Para obtener el código en este punto, realice el pago git Paso-2

Pero hay un error

Si ejecuta este código, arrastrar hacia abajo en la pantalla con el dedo produce
El efecto deseado: la imagen se escala y se recupera. Pero si te desplazas hacia arriba
para leer el texto ... bueno, te darás cuenta de que no puedes.

¿Por qué?

Porque cuando nos desplazamos hacia arriba, estamos comprimiendo el UIImageView en un
línea de altura cero. Su parte superior debe ser igual a la parte superior de la vista y su parte inferior debe ser igual a la parte superior de la vista de respaldo de texto. Esto significa que la vista de desplazamiento continuará "desplazándose", pero no veremos los cambios. Esto se debe a que la vista de respaldo está atascada contra la vista de imagen, que se niega a moverse por encima de la parte superior de la vista raíz a pesar del desplazamiento de la vista de desplazamiento.

El diseño automático está resolviendo técnicamente nuestras limitaciones, pero no es lo que queremos.

Arreglando el error

Tenemos que cambiar cómo restringimos la vista de la imagen. Aquí está el cambio:

imageView.snp.makeConstraints {
 hacer en
make.left.right.equalTo (imageContainer)
// ** Tenga en cuenta las prioridades
 make.top.equalTo (ver) .priority (.high)
// ** También agregamos una restricción de altura
 make.height.greaterThanOrEqualTo (imageContainer.snp.height) .priority (.required)
// ** Y mantener la restricción inferior
 make.bottom.equalTo (imageContainer.snp.bottom)
}

Observe cómo ahora tenemos una restricción superior, una restricción inferior y una altura
¿restricción? Esta es una de las cosas asombrosas sobre el diseño automático: podemos tener
restricciones conflictivas y se romperán en orden de prioridad. Esto es necesario para lograr el efecto que queremos.

Primero, mantenemos nuestra restricción original. La parte superior de nuestra vista de imagen está anclada a
La parte superior de nuestra vista. Le damos a esto una prioridad de .high.

Luego agregamos una restricción adicional: la altura de nuestra imagen debe ser mayor
igual o igual que la altura del contenedor de imágenes detrás de él (recuerde que nuestro contenedor de imágenes tiene la restricción de relación de aspecto). Esto tiene una prioridad requerida.

Entonces, ¿qué sucede cuando nos desplazamos hacia arriba?

Bueno, la imagen no puede ser más pequeña. Nuestra restricción de altura tiene mayor prioridad
que la restricción superior. Entonces, cuando nos desplazamos hacia arriba, el diseño automático romperá el
restricción de prioridad más baja para resolver el sistema. Romperá la cima
restricción, y nuestro comportamiento de desplazamiento volverá a la normalidad. Esto nos permite
para desplazarse hacia arriba y leer el texto.

Tenga en cuenta que también puede eliminar la restricción de altura en esta instancia y simplemente establecer la prioridad de restricción superior en .high. Esto permitirá que iOS rompa la restricción superior y comprima la vista de la imagen a altura cero. Dado que
 Modo de contenido .scaleAspectFill, esto crea un efecto de paralaje. Pruébalo. Es posible que prefiera la forma en que se ve.

Nota: Para obtener el código en este punto, haga git checkout Paso 3

Pulir los detalles

Hay tres problemas discordantes que debemos solucionar mientras estamos aquí.

1. Desplazamiento excesivo de texto

Si nos desplazamos más allá de la parte inferior de la vista, podemos ver lo feo
Fondo gris de nuestro controlador de vista. Podemos usar exactamente el mismo método para
estiramos nuestra vista de respaldo cuando nos desplazamos sobre la parte inferior de la vista.

No entraré en el código, ya que es básicamente la misma técnica que la vista de imagen anterior. Agregamos una vista de respaldo de texto adicional detrás de nuestro contenedor de texto y luego fijamos su borde inferior al borde inferior de la vista raíz.

Nota: Para obtener el código en este punto, haga git checkout Paso-4

2. Respetando el área segura

En el iPhone X, nuestro texto se superpone al indicador de inicio. Desactivamos el ajuste automático de inserción de contenido de nuestra vista de desplazamiento para permitir que el contenido de nuestra imagen vaya directamente a la parte superior de la pantalla. Por lo tanto, tendremos que manejar manualmente el recuadro inferior utilizando la nueva propiedad safeAreaInsets en iOS 11.

También queremos usar safeAreaInsets para ajustar los indicadores de desplazamiento de nuestra vista de desplazamiento. De esta manera, no se toparán con los bordes curvos de la pantalla en el iPhone X.

Para solucionar estos dos problemas, anularemos viewDidLayoutSubviews y estableceremos manualmente el recuadro inferior de la vista de desplazamiento. iOS 11 normalmente haría esto automáticamente, pero * no * queremos insertar la parte superior. Queremos que nuestra imagen de encabezado esté al ras detrás de la barra de estado.

Le hemos dicho a iOS 11 que no toque nuestra vista de desplazamiento configurando su
contentInsetAdjustmentBehavior a .never.

anular func viewDidLayoutSubviews () {
 super.viewDidLayoutSubviews ()
// ** Queremos que los indicadores de desplazamiento utilicen todas las inserciones de área segura
 scrollView.scrollIndicatorInsets = view.safeAreaInsets
// ** Pero queremos que el contenido real solo respete el fondo seguro
 scrollView.contentInset = UIEdgeInsets (arriba: 0, izquierda: 0, abajo: view.safeAreaInsets.bottom, derecha: 0)
}

Esto nos da la siguiente apariencia cuando se desplaza hasta el final.
Tenga en cuenta que el indicador de desplazamiento ya no se pierde detrás de la curva, y obtenemos
mucho más espacio por encima del indicador de inicio.

Nota: Para obtener el código en este punto, realice el pago git Paso 5

3. Ocultar la barra de estado cuando sea necesario

Nuestro texto se superpone a la barra de estado cuando nos desplazamos hacia arriba. Esto se ve asqueroso.

Ocultemos la barra de estado con una animación genial cuando el usuario desplaza el texto al área de la barra de estado. Es bastante fácil detectar esto, y creo que el efecto se ve muy bien.

Cómo hacemos esto?

  • Convertimos el texto del Contenedor a coordenadas de pantalla.
  • Verificamos si la Y mínima de ese marco es menor que la inserción del área segura superior de la vista. Esto indica que el contenedor de texto se está moviendo al área de la barra de estado.
  • Si es así, ocultamos la barra de estado. Si no, mostramos la barra de estado.

Realizamos esta comprobación en el método scrollViewDidScroll (_ :) del
UIScrollViewDelegate. Entonces, hacemos que nuestro StretchyViewController implemente este delegado y se establezca como el delegado para su vista de desplazamiento.

Aquí está el código para la verificación de la barra de estado:

// MARK: - Vista de delegado de desplazamiento
private var previousStatusBarHidden = false
func scrollViewDidScroll (_ scrollView: UIScrollView) {
 // ** Mantenemos el estado oculto de la barra de estado anterior para que
 // no estamos activando un bloque de animación implícito para cada cuadro
 // en el que se desplaza la vista de desplazamiento
 if previousStatusBarHidden! = shouldHideStatusBar {
UIView.animate (withDuration: 0.2, animaciones: {
 self.setNeedsStatusBarAppearanceUpdate ()
 })
previousStatusBarHidden = shouldHideStatusBar
 }
}
// MARK: - Apariencia de la barra de estado
anular var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
 // ** Usamos la animación de diapositiva porque funciona bien con el desplazamiento
 volver .slide
}
anular var prefersStatusBarHidden: Bool {
 return deberíaHideStatusBar
}
var privado debeHideStatusBar: Bool {
 // ** Aquí es donde calculamos si nuestro contenedor de texto
 // va a llegar al área segura superior
 let frame = textContainer.convert (textContainer.bounds, a: nil)
 return frame.minY 

Nota: Para obtener el código en este punto, realice el pago de git Paso 6

Lo que hemos cubierto

  • Puedes fijar casi cualquier cosa a cualquier otra cosa, y tus puntos de vista
    estirar para satisfacer sus limitaciones.
  • Esto funcionará incluso si te desplazas.
  • Las restricciones están rotas en orden de prioridad, así que no tengas miedo de experimentar
    con restricciones en conflicto.
  • ¡Usa SnapKit!

Simeon Saëns lidera las actividades de desarrollo móvil de Enabled con un fuerte enfoque en el diseño y la interacción humano-computadora. Simeon también está llamado a reunirse con clientes para comprender sus necesidades y desarrollar soluciones técnicas.
Sigue a Simeón en Twitter

¿Tienes más preguntas? Tweet a nosotros @EnabledHQ