Auto Layout: restricciones, prioridades y el motor
Auto Layout no coloca cajas: resuelve sistemas de ecuaciones e inecuaciones lineales con un algoritmo de simplex incremental. Esta lección explica el motor por dentro —variables, relaciones, prioridades, tamaño intrínseco, resistencia y abrazo del contenido—, enseña a diagnosticar ambigüedad e insatisfacibilidad como lo que son, y usa ese conocimiento para entender por contraste qué eligió hacer SwiftUI cuando decidió no usar un solucionador.
De todo lo que UIKit aporta al repertorio de alguien que trabaja en declarativo, Auto Layout es la pieza con mayor densidad conceptual y la peor explicada. La mayoría de tutoriales lo presentan como una forma de pegar bordes a otros bordes, y con esa descripción se puede construir una pantalla sencilla y no se puede entender absolutamente nada de lo que ocurre cuando el sistema se queja. La descripción correcta es distinta y sorprendentemente formal: Auto Layout es un solucionador de sistemas de restricciones lineales, en el que cada vista aporta cuatro variables reales, cada regla que escribes aporta una ecuación o una inecuación, y el motor busca en cada ciclo de layout una asignación que satisfaga todas las restricciones obligatorias mientras minimiza el incumplimiento de las opcionales según su peso. Entender esa maquinaria cambia por completo la relación con el framework, y de paso explica por qué SwiftUI decidió no adoptarla.
- Describir el modelo formal de
Auto Layoutcomo sistema de restricciones y no como colocación de cajas. - Manejar prioridades, tamaño intrínseco, resistencia a la compresión y abrazo del contenido como un mecanismo único.
- Diagnosticar ambigüedad e insatisfacibilidad leyendo los diagnósticos del motor en lugar de probar al azar.
- Contrastar el solucionador con la negociación de tamaño de SwiftUI y saber qué se ganó y qué se perdió.
Un solucionador, no un colocador
El punto de partida es que cada vista expone cuatro incógnitas independientes —origen horizontal, origen vertical, anchura y altura— y que todo lo demás son atributos derivados. El borde derecho no es una variable: es el origen horizontal más la anchura. El centro tampoco: es el origen más la mitad de la dimensión. Esa reducción importa porque explica por qué el motor necesita exactamente dos restricciones no redundantes por eje y por vista para tener solución única, ni una más ni una menos.
Cada restricción es una relación afín entre dos atributos con la forma canónica que Apple documenta desde el primer día: un atributo es igual, mayor o igual, o menor o igual que otro atributo multiplicado por un factor y desplazado por una constante. Las inecuaciones son lo que convierte esto en un problema interesante, porque introducen holgura y permiten expresar intenciones como al menos este margen o como mucho esta anchura sin fijar un valor.
// La forma canonica, escrita con anclas en lugar de con la API antigua
NSLayoutConstraint.activate([
tarjeta.leadingAnchor.constraint(equalTo: guia.leadingAnchor, constant: 16),
tarjeta.trailingAnchor.constraint(equalTo: guia.trailingAnchor, constant: -16),
tarjeta.topAnchor.constraint(greaterThanOrEqualTo: guia.topAnchor, constant: 8),
tarjeta.heightAnchor.constraint(equalTo: tarjeta.widthAnchor, multiplier: 0.618)
])
El algoritmo que resuelve ese sistema es Cassowary, publicado a finales de los noventa por Badros y Borning, y su virtud no es resolver sistemas lineales —eso lo hace cualquier simplex— sino resolverlos de forma incremental y con restricciones de distinta fuerza. Incremental significa que añadir, eliminar o cambiar una restricción no obliga a recalcular todo desde cero, que es lo que permite animar una constante a sesenta cuadros por segundo. Y las fuerzas son lo que traduce las prioridades del framework a un problema de optimización bien definido, en el que las obligatorias son restricciones duras y las demás términos penalizados en la función objetivo.
La linealidad no es una limitación arbitraria sino la condición que hace que el problema sea resoluble en tiempo razonable y que la solución sea única cuando el sistema está bien planteado. En cuanto se admiten relaciones no lineales —una anchura proporcional a la raíz de otra, un alto que dependa de un condicional— se abandona el territorio del simplex. Por eso todo lo que no es afín en UIKit se resuelve fuera del motor, con código imperativo en los métodos de layout.
Prioridades y tamaño intrínseco
Las prioridades son un entero de uno a mil, donde mil significa obligatorio y todo lo demás significa deseable con un peso. Los tres valores con nombre —doscientos cincuenta, setecientos cincuenta y mil— no tienen nada de mágico: son simplemente convenciones para que el sistema tenga margen de maniobra por encima y por debajo. La regla práctica es no usar nunca dos prioridades iguales cuando quieres que una gane, porque en ese caso el resultado depende del orden de resolución y deja de ser determinista a efectos de tu razonamiento.
El concepto que hace que todo encaje es el tamaño intrínseco. Una etiqueta, un botón o una imagen saben cuánto quieren medir en función de su contenido, y ese deseo no se expresa como restricción sino como un par de valores que el motor convierte internamente en restricciones con prioridad configurable. Ahí aparecen los dos parámetros peor nombrados del framework y que en realidad son perfectamente simétricos.
Abrazo del contenido
Cuánto se resiste una vista a crecer por encima de su tamaño natural. Prioridad alta significa quiero quedarme pequeña. Es el parámetro que decide, entre dos etiquetas en una fila, cuál absorbe el espacio sobrante.
Resistencia a la compresion
Cuánto se resiste a encogerse por debajo de su tamaño natural. Prioridad alta significa no me recortes. Es el parámetro que decide cuál de dos textos se trunca cuando no caben los dos.
Tamano intrinseco
El deseo que la vista declara. Si vale la constante de dimensión desconocida en un eje, el motor exige que ese eje quede determinado por restricciones externas o el layout será ambiguo.
Con estos tres conceptos se explica el noventa por ciento de los comportamientos que parecen caprichosos. Dos etiquetas contiguas que no se reparten el espacio como esperabas tienen el mismo abrazo del contenido y el motor desempata de forma que a ti te parece arbitraria. Un texto que se trunca cuando debería truncarse el otro tiene menos resistencia a la compresión. Una vista personalizada que colapsa a tamaño cero no implementa tamaño intrínseco y nadie le ha dado restricciones de dimensión.
// Decidir explicitamente quien cede en lugar de dejarlo al azar
titulo.setContentCompressionResistancePriority(.required, for: .horizontal)
subtitulo.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
subtitulo.setContentHuggingPriority(.defaultHigh, for: .horizontal)
Diagnosticar el sistema
Un sistema de restricciones puede fallar de dos maneras y confundirlas lleva a arreglar lo que no está roto. La ambigüedad ocurre cuando hay soluciones infinitas porque faltan restricciones: el motor elige una y la pantalla se ve razonable en tu dispositivo y absurda en otro. La insatisfacibilidad ocurre cuando no hay ninguna solución porque las restricciones obligatorias se contradicen: el motor rompe una, la elige él, y registra en consola un informe que casi nadie lee entero.
flowchart TB a[Cambia el estado o la geometria] --> b[setNeedsLayout marca el arbol sucio] b --> c[El motor resuelve el sistema de restricciones] c --> d[Se asignan los frames resultantes] d --> e[layoutSubviews de cada vista implicada] e --> f[Dibujo y composicion] c --> g[Si no hay solucion se rompe una restriccion y se registra]
Ese informe de consola es una herramienta de diagnóstico de primer orden y merece leerse como lo que es: la lista completa de restricciones implicadas en el conflicto, más la que el motor decidió sacrificar. La técnica que convierte el informe en algo legible consiste en asignar identificadores a las restricciones antes de que haya problemas, porque una lista de direcciones de memoria no dice nada y una lista de nombres dice casi todo.
Olvidar la desactivación de la traducción automática de máscaras de redimensionado. Cuando una vista se añade por código sin desactivarla, el sistema genera restricciones a partir de su marco actual y esas restricciones son obligatorias, de modo que chocan con las tuyas de forma inmediata. El síntoma es un conflicto que menciona restricciones que tú no escribiste, y la causa siempre es la misma línea que falta.
La ambigüedad es más traicionera porque no produce ningún aviso. El framework ofrece herramientas específicas para cazarla: una consulta que indica si una vista tiene layout ambiguo y un método de depuración que la sacude visualmente entre soluciones válidas, además del propio depurador de vistas que la señala en la jerarquía. Merece la pena incorporar la comprobación a las pruebas de interfaz de las pantallas críticas, porque una ambigüedad que hoy se resuelve bien puede resolverse mal cuando cambie una fuente, un idioma o un tamaño de pantalla.
Hay finalmente una pieza que evita casi todo el trabajo manual y que conviene conocer antes que las restricciones sueltas: la pila. Una UIStackView no dibuja nada y su única función es generar restricciones por ti según una política de distribución y alineación. Es, con diferencia, la mejor relación entre expresividad y líneas escritas en UIKit, y su parecido con HStack y VStack no es casual sino genealógico.
Qué eligió SwiftUI
Aquí llega el contraste que da sentido a toda la lección. SwiftUI no usa Auto Layout. No es que lo use con otra sintaxis: el motor declarativo implementa un algoritmo completamente distinto, de una sola pasada y sin solucionador, en el que el padre propone un tamaño al hijo, el hijo elige el suyo dentro de esa propuesta y el padre lo coloca. No hay sistema global, no hay ecuaciones y no hay conflictos posibles, porque no existe la categoría de restricción insatisfacible.
Esa decisión de diseño tiene consecuencias en ambas direcciones y merecen enunciarse sin triunfalismo. A favor: el layout es predecible y componible, el coste es lineal en el número de vistas, no hay ambigüedad porque siempre hay una respuesta y no existen los informes de conflicto en consola. En contra: no se pueden expresar relaciones globales entre vistas lejanas, que en Auto Layout son triviales, y por eso SwiftUI necesitó introducir mecanismos adicionales —lectores de geometría, preferencias, guías de alineación personalizadas y el protocolo de layout propio— para recuperar una parte de lo que el solucionador daba gratis.
Lo que separa a Auto Layout de la negociación de SwiftUI no es la sintaxis ni la modernidad: es dónde reside la autoridad sobre la posición final de las cosas, y esa es una decisión arquitectónica con siglos de precedentes en otros dominios. En el modelo de restricciones la autoridad es global y difusa. Ninguna vista sabe dónde va a acabar; el resultado emerge de un sistema que nadie controla por completo y que puede recibir aportaciones de partes muy alejadas del árbol, incluso de código que se ejecutó meses después de escribirse la primera regla. Eso da un poder expresivo enorme —se puede alinear la base de un texto con el centro de una imagen situada en otra rama de la jerarquía— y a cambio destruye la composicionalidad, porque para razonar sobre una vista hay que conocer todas las restricciones del sistema en el que vive. En el modelo de negociación la autoridad es local y jerárquica. Cada padre decide qué ofrece y cada hijo decide qué acepta, la conversación no cruza nunca hacia los lados y el resultado de un subárbol depende únicamente de la propuesta que recibe. Eso permite razonar sobre una vista en aislamiento, reutilizarla en cualquier contexto y garantizar que el coste no explote, a cambio de perder la capacidad de expresar relaciones a distancia. Es exactamente la misma tensión que separa las variables globales de los parámetros de función, el estado compartido de los mensajes entre actores, o el acoplamiento por datos del acoplamiento por interfaz: poder expresivo contra razonamiento local. Apple no cambió de motor porque el simplex fuera lento; cambió porque a partir de cierto tamaño de aplicación el coste de razonar sobre un sistema global supera cualquier ganancia expresiva, y esa es una lección que se aplica muchísimo más allá del layout.
Auto Layout es un solucionador incremental de restricciones lineales con prioridades, no una API de colocación. Cada vista aporta cuatro variables y necesita dos restricciones no redundantes por eje. Las prioridades ordenan lo deseable; el abrazo del contenido decide quién crece y la resistencia a la compresión decide quién se recorta. Ambigüedad significa faltan restricciones e insatisfacibilidad significa sobran restricciones contradictorias, y se diagnostican de forma distinta. SwiftUI no lo usa: implementa una negociación de una pasada, local y componible, que gana en predictibilidad lo que pierde en relaciones a distancia.
- Construye por código una fila con dos etiquetas y un botón, y consigue con solo prioridades que la primera etiqueta se trunque antes que la segunda; después invierte el comportamiento sin tocar las restricciones.
- Provoca deliberadamente un conflicto insatisfacible, lee el informe completo de consola e identifica qué restricción sacrificó el motor y por qué esa y no otra.
- Crea una ambigüedad real y cázala con la consulta de layout ambiguo y con el método de sacudida antes de arreglarla.
- Implementa el tamaño intrínseco de una vista personalizada y comprueba qué cambia cuando devuelves la constante de dimensión desconocida en uno de los ejes.
- Reproduce la misma pantalla en SwiftUI y anota qué restricciones desaparecieron por completo y cuáles tuviste que recuperar con guías de alineación o lectores de geometría.