Cuando construimos interfaces con SwiftUI solemos pensar en la alineación como una propiedad bastante sencilla: un VStack puede alinear su contenido a .leading, .center o .trailing, mientras que un HStack permite decidir cómo se distribuyen verticalmente sus elementos.
Sin embargo, por debajo de esa API aparentemente simple existe un mecanismo bastante más potente: las guías de alineación (alignment guides).
Una guía de alineación representa una coordenada dentro de una vista. El contenedor consulta esa coordenada en cada uno de sus hijos y los posiciona de forma que todas esas referencias coincidan.
Entender este mecanismo permite construir composiciones más precisas sin recurrir a offset, valores mágicos o mediciones manuales con GeometryReader.
Cómo alinea SwiftUI las vistas
Imaginemos dos elementos de tamaños diferentes dentro de un HStack:
HStack {
RoundedRectangle(cornerRadius: 12)
.frame(width: 80, height: 80)
RoundedRectangle(cornerRadius: 12)
.frame(width: 40, height: 40)
}
Por defecto, un HStack utiliza la alineación vertical .center. Eso significa que SwiftUI consulta la posición del centro vertical de cada vista y coloca ambas de forma que esos puntos estén sobre la misma línea horizontal.
Conceptualmente, cada vista expone una coordenada:
Vista grande -> centro = 40
Vista pequeña -> centro = 20
El contenedor no necesita que tengan la misma altura. Simplemente desplaza cada elemento durante el proceso de layout hasta que ambas coordenadas coincidan.
Este detalle es importante porque una guía de alineación no describe dónde debe aparecer una vista en la pantalla. Describe qué punto de esa vista debe coincidir con el punto equivalente de las demás.
Alineaciones integradas en SwiftUI
SwiftUI proporciona varias guías de alineación predefinidas.
En el eje horizontal encontramos:
.leading
.center
.trailing
Mientras que en el eje vertical disponemos, entre otras, de:
.top
.center
.bottom
.firstTextBaseline
.lastTextBaseline
Las dos últimas son especialmente importantes cuando trabajamos con texto.
Por ejemplo:
HStack(alignment: .firstTextBaseline) {
Text("128")
.font(.system(size: 42, weight: .bold))
Text("km")
.font(.body)
}
Si utilizásemos la alineación .center, SwiftUI colocaría ambos textos haciendo coincidir sus centros geométricos. Como los tamaños de fuente son distintos, el resultado puede parecer ligeramente desalineado.
Con .firstTextBaseline, SwiftUI utiliza como referencia la primera línea base tipográfica de cada Text. El número y la unidad quedan entonces alineados como lo estarían dentro de una misma línea de texto.
La diferencia puede ser pequeña en código, pero visualmente suele ser considerable.
ViewDimensions: las coordenadas de una vista
Cuando personalizamos una guía mediante alignmentGuide(_:computeValue:), SwiftUI nos proporciona una instancia de ViewDimensions.
Este tipo contiene las dimensiones de la vista y los valores de sus distintas guías de alineación.
Podemos consultar, por ejemplo:
.alignmentGuide(.center) { dimensions in
dimensions.height / 2
}
O reutilizar otra guía ya existente:
.alignmentGuide(.bottom) { dimensions in
dimensions[.top]
}
En una guía vertical, las coordenadas se miden desde la parte superior de la vista hacia abajo.
Por ejemplo, para una vista de 100 puntos de altura:
.top -> 0
.center -> 50
.bottom -> 100
En las guías horizontales ocurre algo equivalente con respecto al origen horizontal de la vista.
ViewDimensions también permite consultar width y height, de modo que la posición de una guía puede calcularse proporcionalmente al tamaño real del elemento.
Modificar la guía que comunica una vista
El modificador alignmentGuide permite reemplazar el valor que una vista comunica para una guía concreta.
Supongamos que tenemos un icono personalizado junto a un título:
HStack(alignment: .firstTextBaseline) {
Image("weather-icon")
.resizable()
.scaledToFit()
.frame(width: 48, height: 48)
Text("Parcialmente nublado")
.font(.title2)
}
Una imagen personalizada no posee una línea base tipográfica como un Text. Por ese motivo, su alineación con .firstTextBaseline puede no coincidir visualmente con el punto que nos interesa.
Podemos proporcionar nuestra propia referencia:
HStack(alignment: .firstTextBaseline) {
Image("weather-icon")
.resizable()
.scaledToFit()
.frame(width: 48, height: 48)
.alignmentGuide(.firstTextBaseline) { dimensions in
dimensions.height * 0.72
}
Text("Parcialmente nublado")
.font(.title2)
}
Ahora la imagen comunica que su línea de referencia se encuentra aproximadamente al 72 % de su altura.
El HStack continúa usando .firstTextBaseline. Lo único que ha cambiado es el valor que esa imagen entrega para dicha guía.
Esto resulta bastante diferente de aplicar:
.offset(y: 4)
offset modifica la representación final de la vista, pero no cambia la información de alineación que utiliza el contenedor. Una guía de alineación, en cambio, forma parte del propio proceso de layout.
El signo puede resultar contraintuitivo
Uno de los detalles que más confusión genera al empezar a utilizar alignmentGuide es la dirección en la que se mueve una vista.
Por ejemplo:
.alignmentGuide(.top) { dimensions in
dimensions[.top] + 20
}
Podría parecer lógico esperar que la vista se desplazase 20 puntos hacia abajo. Sin embargo, el contenedor debe mover la vista en dirección contraria para conseguir que esa nueva coordenada coincida con la guía común. No estamos diciendo «mueve esta vista 20 puntos». Estamos diciendo «considera que el punto usado para .top se encuentra 20 puntos más abajo dentro de esta vista».
El contenedor calcula después la posición necesaria para hacer coincidir ese punto con los del resto de elementos.Pensar en coordenadas de referencia, en lugar de desplazamientos, hace que el comportamiento resulte mucho más sencillo de entender.
Crear una guía de alineación personalizada
Las guías estándar cubren muchos diseños, pero no todos. Imaginemos una fila para una aplicación de música. Queremos alinear la duración de una canción y un botón de reproducción con el título, aunque el título se encuentre dentro de un VStack junto al nombre del artista:
HStack {
Text("03:42")
Button {
play()
} label: {
Image(systemName: "play.fill")
}
VStack(alignment: .leading) {
Text("Northern Lights")
.font(.headline)
Text("The Satellites")
.font(.caption)
.foregroundStyle(.secondary)
}
}
El problema es que el HStack solamente conoce a sus hijos directos. El Text que contiene el título está dentro del VStack. Podemos definir una guía que represente específicamente la línea base del título. Para hacerlo necesitamos un tipo que adopte AlignmentID:
private struct SongTitleAlignment: AlignmentID {
static func defaultValue(
in dimensions: ViewDimensions
) -> CGFloat {
dimensions[VerticalAlignment.firstTextBaseline]
}
}
Después exponemos esa guía mediante una extensión de VerticalAlignment:
extension VerticalAlignment {
static let songTitle = VerticalAlignment(
SongTitleAlignment.self
)
}
A partir de ese momento podemos utilizar .songTitle como cualquier otra alineación de SwiftUI:
HStack(alignment: .songTitle) {
Text("03:42")
Button {
play()
} label: {
Image(systemName: "play.fill")
}
VStack(alignment: .leading) {
Text("Northern Lights")
.font(.headline)
.alignmentGuide(.songTitle) { dimensions in
dimensions[VerticalAlignment.firstTextBaseline]
}
Text("The Satellites")
.font(.caption)
.foregroundStyle(.secondary)
}
}
El Text del título declara explícitamente qué coordenada debe utilizar para .songTitle. Aquí aparece una de las características más interesantes del sistema de alineación de SwiftUI.
Las guías pueden propagarse por la jerarquía
El Text que define .songTitle no es hijo directo del HStack. Está contenido dentro de un VStack. Aun así, cuando el HStack solicita al VStack el valor de la guía .songTitle, SwiftUI puede obtener el valor explícito definido por uno de sus descendientes y utilizarlo como referencia del contenedor.
En nuestro ejemplo, la línea base del título termina funcionando como una referencia que atraviesa la jerarquía. Esto permite describir relaciones entre elementos que están en diferentes niveles sin tener que calcular posiciones absolutas. Es precisamente el tipo de problema para el que, de otro modo, sería tentador utilizar GeometryReader, preferencias o mediciones manuales.
El valor por defecto de una guía personalizada
El método:
static func defaultValue(
in dimensions: ViewDimensions
) -> CGFloat
define qué debe ocurrir cuando una vista no proporciona explícitamente un valor para nuestra guía. Podemos basarlo en cualquiera de las guías existentes:
dimensions[VerticalAlignment.center]
o calcular una posición propia:
dimensions.height / 3
Por ejemplo:
private struct BadgeAlignment: AlignmentID {
static func defaultValue(
in dimensions: ViewDimensions
) -> CGFloat {
dimensions.height * 0.25
}
}
De esta forma, las vistas que no especifiquen otro valor utilizarán como referencia un punto situado a un cuarto de su altura. Elegir un buen valor por defecto hace que una guía personalizada pueda reutilizarse sin llenar todas las vistas de modificadores adicionales.
Guías horizontales personalizadas
El mismo sistema funciona en el eje horizontal. Supongamos una pantalla con varias filas donde queremos que el valor principal comience exactamente en el mismo punto, independientemente de la longitud de la etiqueta.
Podemos crear nuestra propia referencia:
private struct ValueColumnAlignment: AlignmentID {
static func defaultValue(
in dimensions: ViewDimensions
) -> CGFloat {
dimensions[HorizontalAlignment.leading]
}
}
extension HorizontalAlignment {
static let valueColumn = HorizontalAlignment(
ValueColumnAlignment.self
)
}
Después podemos marcar el punto relevante:
VStack(alignment: .valueColumn, spacing: 12) {
HStack {
Text("Temperatura")
Text("21 ºC")
.alignmentGuide(.valueColumn) { dimensions in
dimensions[.leading]
}
}
HStack {
Text("Humedad relativa")
Text("48 %")
.alignmentGuide(.valueColumn) { dimensions in
dimensions[.leading]
}
}
}
La idea sigue siendo la misma: el contenedor alinea coordenadas significativas del contenido, no necesariamente los bordes de las vistas completas.
Alignment combina los dos ejes
HorizontalAlignment y VerticalAlignment trabajan sobre un único eje, pero SwiftUI también dispone del tipo Alignment.
Un Alignment combina una guía horizontal y otra vertical:
let customAlignment = Alignment(
horizontal: .leading,
vertical: .firstTextBaseline
)
Este tipo aparece, por ejemplo, en ZStack, overlay y background.
Text("Sin conexión")
.padding()
.overlay(alignment: .topTrailing) {
Image(systemName: "wifi.slash")
}
En estos casos SwiftUI necesita conocer simultáneamente la referencia horizontal y la vertical para colocar una vista sobre otra.
Cuándo utilizar guías y cuándo no
Las guías de alineación son especialmente útiles cuando queremos expresar una relación geométrica entre varias vistas.
Son una buena opción para:
- Alinear textos con tamaños de fuente diferentes.
- Integrar ilustraciones personalizadas junto a texto.
- Alinear un elemento con contenido que se encuentra dentro de otro contenedor.
- Crear columnas visuales que no coinciden con los límites de las vistas.
- Mantener un diseño adaptable sin depender de tamaños concretos.
No siempre son necesarias. Si solamente queremos añadir una separación, padding suele ser suficiente. Si queremos superponer elementos, overlay o ZStack pueden expresar mejor la intención. Y cuando necesitamos implementar por completo un algoritmo de distribución propio, el protocolo Layout ofrece un nivel de control mayor.
Las guías resultan especialmente interesantes en el punto intermedio: cuando los contenedores estándar de SwiftUI ya ofrecen la estructura adecuada, pero necesitamos indicar con más precisión qué partes de sus hijos deben alinearse.
Evitar valores mágicos
Es posible escribir algo como:
.alignmentGuide(.center) { _ in
17
}
pero esa constante probablemente dejará de tener sentido en cuanto cambie el contenido, el tamaño dinámico del texto o la localización.
Siempre que sea posible es preferible derivar la posición de ViewDimensions:
.alignmentGuide(.center) { dimensions in
dimensions.height * 0.35
}
o reutilizar otra guía semántica:
.alignmentGuide(.center) { dimensions in
dimensions[.firstTextBaseline]
}
El segundo enfoque suele ser aún mejor porque expresa la intención del diseño y se adapta automáticamente a cambios tipográficos.
Accesibilidad y Dynamic Type
Una ventaja importante de utilizar alineaciones semánticas es que el diseño puede responder mejor a cambios de tamaño de fuente.
Un desplazamiento fijo:
.offset(y: 6)
seguirá teniendo exactamente seis puntos aunque el usuario aumente considerablemente el tamaño del texto.
Una alineación basada en:
dimensions[.firstTextBaseline]
se recalcula durante el layout utilizando las dimensiones reales de la vista.
Esto no significa que cualquier composición vaya a funcionar automáticamente con todos los tamaños de Dynamic Type, pero reduce la dependencia de ajustes visuales ligados a unas dimensiones concretas.
Una herramienta de layout más importante de lo que parece
Las guías de alineación suelen quedar eclipsadas por APIs más visibles como Grid, GeometryReader o el protocolo Layout, pero forman parte de uno de los mecanismos fundamentales de posicionamiento de SwiftUI. La clave está en cambiar la forma de pensar sobre ellas.
Una guía no dice a SwiftUI cuánto debe desplazar una vista. Define un punto significativo dentro de ella. El contenedor compara ese punto con el de sus hermanos y decide dónde colocar cada elemento.
Con las guías integradas podemos resolver la mayoría de alineaciones habituales. Con alignmentGuide podemos corregir el punto que comunica una vista concreta. Y mediante AlignmentID podemos crear referencias semánticas capaces incluso de propagarse a través de contenedores intermedios.
Cuando un diseño empieza a llenarse de offset, cálculos manuales y números escogidos a ojo, merece la pena comprobar si el problema que estamos intentando resolver no es, en realidad, un problema de alineación.