frame y sus trampas: fijar no es imponer
El modificador frame es el más usado y el peor entendido del framework. No fija el tamaño de una vista: crea un contenedor de ese tamaño que propone hacia dentro y alinea lo que reciba. Esta lección separa el frame rígido del flexible, reconstruye qué propone y qué devuelve cada uno, y explica por qué encadenar dos frames o cambiar el orden de dos modificadores produce dibujos completamente distintos.
Hay una frase que todo el mundo dice y que es falsa: le puse un frame para fijarle el tamaño. El modificador frame no fija el tamaño de nada, porque en el modelo de la lección anterior ningún padre tiene autoridad para hacerlo. Lo que hace es mucho más modesto y mucho más interesante: inserta en el árbol una vista contenedora nueva, cuyo tamaño es el que tú pediste, que propone ese tamaño a su hijo y después coloca al hijo dentro según una alineación. Si el hijo obedece, el resultado se parece a fijar. Si el hijo no obedece —y una imagen sin resizable nunca obedece—, el contenedor mide lo que pediste y el hijo se dibuja desbordado por encima. Entender esa diferencia entre imponer y proponer es lo que separa a quien pelea con el layout de quien lo dirige.
- Describir
framecomo una vista contenedora con tamaño propio, y no como una mutación de la vista modificada. - Distinguir el frame rígido del flexible y enunciar qué propone cada uno a su hijo.
- Interpretar el parámetro de alineación y saber qué eje gobierna en cada caso.
- Predecir el resultado de encadenar dos frames y de permutar
frameconpaddingobackground.
El frame rígido
La forma con width y height explícitos hace exactamente tres cosas, en este orden. Primero, propone a su hijo el tamaño indicado; las dimensiones que no especifiques se dejan pasar tal como venían de arriba. Segundo, devuelve a su propio padre el tamaño que le pediste, con independencia total de lo que el hijo haya respondido. Tercero, coloca al hijo dentro de ese rectángulo según la alineación, que por omisión es el centro.
Image("montana") // intrinseca: 800 x 600 puntos
.frame(width: 100, height: 100)
Aquí el frame propone cien por cien, la imagen ignora la propuesta y devuelve ochocientos por seiscientos, el frame informa hacia arriba de que mide cien por cien, y luego centra una imagen de ochocientos por seiscientos dentro de un cuadrado de cien. El resultado es una imagen desbordada y centrada, con el resto del layout comportándose como si midiera cien. No hay error ni aviso: el sistema hizo exactamente lo que le pediste. Añadir .clipped() recorta el dibujo al rectángulo del frame; añadir .resizable() a la imagen la convierte en una vista que sí acepta la propuesta. Son remedios distintos para dos problemas distintos.
frame decide tamaños, no ventanas de dibujo. El recorte es un efecto visual independiente que aporta clipped o clipShape. Confundirlos lleva a la conclusión errónea de que el frame no funcionó.
Que las dos dimensiones sean opcionales tiene un uso constante y poco comentado: fijar un eje y dejar el otro libre. Es la forma correcta de imponer una altura de fila sin decidir nada sobre el ancho, que seguirá adaptándose al contenedor:
Text("Fila de altura fija")
.frame(height: 44) // el ancho propuesto pasa intacto al hijo
Aquí el frame propone al Text la altura de cuarenta y cuatro y el ancho que le llegara de arriba, sin tocarlo. Hacia arriba devuelve cuarenta y cuatro de alto y el ancho que el Text haya elegido. Es un frame rígido en un eje y transparente en el otro, y esa mezcla es el modo en que se usa la mayor parte de las veces en código real.
El frame flexible
La sobrecarga con minWidth, idealWidth, maxWidth y sus equivalentes verticales es una vista distinta con un algoritmo distinto, y es la que causa casi todas las dudas. Su comportamiento por eje se puede resumir en tres reglas encadenadas:
- La propuesta que recibe se acota al intervalo entre el mínimo y el máximo, y esa propuesta acotada es la que baja al hijo. Si la propuesta entrante es
nil, se sustituye por el valor ideal cuando lo hayas dado. - El tamaño que devuelve hacia arriba también queda acotado a ese mismo intervalo. Dentro del intervalo sigue a la propuesta; si no hubo propuesta, cae en el ideal, y a falta de ideal, en el tamaño que devolvió el hijo.
- Los extremos que no especificas no imponen nada: solo actúan las cotas que escribiste.
Conviene ver las tres reglas actuando sobre un caso concreto. Supón un padre que propone quinientos de ancho:
Text("corto")
.frame(minWidth: 100, maxWidth: 300)
La propuesta de quinientos se acota a trescientos y eso es lo que baja al Text, que devolverá lo suyo, pongamos cuarenta. Hacia arriba, el frame no informa de cuarenta ni de quinientos: informa de trescientos, porque dentro del intervalo sigue a la propuesta y la propuesta acotada era trescientos. Con un padre que propusiera cincuenta, el resultado habría sido cien, el suelo. El tamaño del hijo, en ambos casos, no ha intervenido en el cálculo.
De ahí se sigue la lectura correcta del idioma más repetido de SwiftUI: maxWidth: .infinity no significa hacerse infinitamente ancho. Significa no pongas techo a mi crecimiento en ese eje, y como la regla dice que dentro del intervalo se sigue la propuesta, el efecto real es ocupar todo lo que el padre proponga. Por eso funciona para que un Text llene el ancho disponible, y por eso deja de funcionar dentro de un ScrollView horizontal, donde no hay propuesta finita que seguir.
min sin max
Establece un suelo. La vista crece con la propuesta pero nunca baja del mínimo, aunque su contenido sea diminuto. Es la herramienta para botones que no deben encogerse por debajo de un tamaño táctil.
ideal a solas
Solo se nota cuando la propuesta es nil: dentro de un ScrollView en el eje del scroll, o bajo fixedSize. En cualquier otro contexto pasa inadvertido, lo que lo convierte en el parámetro más olvidado del modificador.
max infinito
Quita el techo y hace que la vista siga la propuesta hasta donde llegue. No genera tamaño de la nada: si nadie propone nada finito, no hay nada a lo que crecer.
Alineación dentro del frame
El parámetro alignment responde a una única pregunta: dónde queda el hijo dentro del rectángulo del frame cuando le sobra o le falta sitio. No tiene ningún efecto si el hijo ocupa exactamente el rectángulo, y no tiene nada que ver con cómo se alinean las líneas dentro de un texto, que es competencia de multilineTextAlignment. Confundir ambos produce la queja clásica de que la alineación no hace nada:
Text("Dos lineas de texto bastante largas para que se parta")
.multilineTextAlignment(.leading) // como se alinean las lineas entre si
.frame(maxWidth: .infinity, alignment: .leading) // donde queda el bloque
El primer modificador gobierna el interior del bloque de texto; el segundo gobierna la posición del bloque dentro del espacio sobrante. Son dos decisiones independientes y a menudo hacen falta las dos.
Hay un uso del frame flexible que no tiene nada que ver con el aspecto y sí con la interacción. Una fila entera de una lista que responda al toque solo en el ancho de su etiqueta es una fila mal construida, y la solución idiomática es expandir el área y declararla táctil:
Text("Ajustes")
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture { abrir() }
Sin contentShape, el área sensible sigue siendo la del texto aunque el frame ocupe toda la fila, porque la forma de contacto por omisión de un Text es el texto y no su marco. Es otro recordatorio de que el frame decide geometría de layout y nada más: ni recorta, ni pinta, ni captura toques.
Por qué el orden lo cambia todo
Cada modificador es un padre nuevo que envuelve a lo que está encima de él en el código. Cambiar el orden cambia el árbol, y por tanto cambia la conversación entera. El caso didáctico son dos frames seguidos:
Text("hola")
.frame(width: 100, height: 100)
.frame(width: 200, height: 200)
El frame de doscientos propone doscientos al frame de cien. El frame de cien ignora esa propuesta —es rígido— y devuelve cien. El frame de doscientos informa de doscientos y centra dentro un cuadrado de cien, que a su vez centra el texto. Hay dos rectángulos concéntricos, y el interior es inalcanzable desde fuera: un frame rígido es una pared opaca a la negociación.
flowchart TB padre[El padre propone 200 x 200] --> f2[Frame externo de 200] f2 --> f1[Frame interno de 100 ignora la propuesta] f1 --> texto[Text elige su tamano dentro de 100] texto --> r1[El frame interno devuelve 100] r1 --> r2[El frame externo devuelve 200 y centra] style padre fill:#f5c2e7,color:#11111b style f1 fill:#f9e2af,color:#11111b style r2 fill:#a6e3a1,color:#11111b
De esa opacidad se sigue una consecuencia práctica que conviene anticipar: dos frames encadenados nunca se combinan ni se anulan, se apilan. No existe forma de relajar desde fuera un ancho fijo puesto dentro, del mismo modo que no existe forma de que un padre encoja a un hijo terco. Si un componente reutilizable trae un frame rígido dentro, quien lo use no podrá adaptarlo, y por eso los componentes bien diseñados dejan la geometría en manos de quien los coloca.
La misma lógica explica las permutaciones con padding y background. Con .padding(20).frame(width: 100) el frame mide cien y dentro va un contenido de sesenta más los márgenes; con .frame(width: 100).padding(20) el resultado mide ciento cuarenta, porque el padding envuelve a un frame ya cerrado de cien. Y con .background(.red).frame(width: 200) el rojo cubre solo el tamaño del contenido, mientras que .frame(width: 200).background(.red) pinta los doscientos enteros. En los tres casos la regla es la misma: lee de abajo arriba en el código, que es de dentro afuera en el árbol.
Antes de discutir con un layout, escribe el árbol de envoltorios en una servilleta, empezando por la vista de contenido y añadiendo un padre por cada línea de modificador. Casi todos los errores de frame desaparecen cuando el árbol está dibujado.
Que un frame rígido bloquee la propuesta entrante parece un defecto y es en realidad el mecanismo que hace componible el sistema entero. Recuerda el problema que Auto Layout no supo resolver: en un solucionador global, insertar una vista en un contenedor puede alterar los números de vistas que no tienen ninguna relación con ella, porque todas las restricciones viven en el mismo sistema de ecuaciones. Para que la composición funcione hace falta poder decir lo que hay debajo de aquí ya no le concierne a nadie de arriba, y eso es exactamente un frame rígido: una barrera de información que corta la propagación de la propuesta hacia abajo y la propagación del tamaño hacia arriba. Es la misma idea que en la teoría de tipos se llama anotación de tipo en una posición de comprobación: al escribir el tipo explícitamente, cortas la inferencia y conviertes un problema global en dos problemas locales independientes. En ambos casos pagas expresividad —el hijo pierde la información sobre el espacio real disponible, igual que la anotación impide que el uso informe a la definición— y compras a cambio dos propiedades que valen mucho más: razonamiento local, porque puedes entender el subárbol sin mirar hacia arriba, y estabilidad de rendimiento, porque un cambio en un rincón no puede desencadenar un recálculo en el rincón opuesto. La consecuencia práctica es una heurística de diseño que conviene interiorizar pronto: pon frames rígidos en las fronteras deliberadas de tu jerarquía, donde de verdad quieras clausurar la negociación, y usa frames flexibles en todo lo demás, donde quieras que la información del espacio disponible siga fluyendo. Un árbol lleno de anchos y altos fijos no es un árbol controlado: es un árbol al que le has amputado la capacidad de adaptarse al Dynamic Type, a la rotación, al iPad y al idioma alemán.
frame no fija tamaños: crea un contenedor que propone hacia dentro, devuelve su propio tamaño hacia arriba y alinea al hijo en el hueco. El rígido devuelve siempre lo que pediste y es opaco a la propuesta entrante; el flexible acota la propuesta y el resultado al intervalo entre mínimo y máximo, siguiendo la propuesta dentro de él. Un máximo infinito significa crecer con lo propuesto, no crecer sin límite. El orden de los modificadores define el árbol de envoltorios, y por tanto el resultado.
- Coloca una
Imagesinresizableen unframepequeño y añade despuésclipped; explica qué cambia en el tamaño y qué cambia solo en el dibujo. - Compara
frame(maxWidth: .infinity)dentro de unVStacky dentro de unScrollViewhorizontal, y razona la diferencia con la regla de la propuesta. - Encadena tres frames rígidos de tamaños decrecientes y predice a mano los tres tamaños devueltos antes de ejecutar.
- Escribe las cuatro permutaciones de
paddingybackgroundalrededor de unframey anota cuál pinta el fondo sobre los márgenes. - Da a un
TextunidealWidthy comprueba en qué contextos se nota y en cuáles no aparece ningún efecto.