Pilas en SwiftUI: VStack, HStack y ZStack para crear layouts reales

- 👤 Andrés Cruz

🇺🇸 In english

Pilas en SwiftUI: VStack, HStack y ZStack para crear layouts reales

Cuando creamos un proyecto en SwiftUI, Xcode nos genera por defecto una vista llamada ContentView. Da igual si luego la renombras o no: lo importante es entender que ese archivo representa una pantalla de tu app. Puede ser un login, un listado, un detalle, lo que sea.

Dentro de esa vista hay una propiedad clave: body. Ahí es donde definimos la interfaz… y donde suele aparecer el primer choque con SwiftUI.

En mi caso, el primer error fue intentar algo tan simple como poner dos Text uno debajo del otro. Parecía trivial, pero SwiftUI no lo permitió. ¿Por qué? Porque SwiftUI solo admite una vista raíz. Y es justo ahí donde entran en juego las pilas o stacks.

Antes vimos como usar las imágenes, textos y vstack en SwiftUI.

Por qué SwiftUI necesita pilas para el layout?

SwiftUI está pensado para describir interfaces de forma declarativa. Eso implica una regla fundamental:
cada vista debe devolver una sola vista raíz.

Si intentas hacer esto:

var body: some View {
   Text("Hola")
   Text("Mundo")
}

No va a funcionar como esperas. Y este es uno de esos momentos en los que muchos pensamos que estamos haciendo algo mal, cuando en realidad nos falta el contenedor adecuado.

Las pilas existen para resolver exactamente este problema: permitirnos agrupar múltiples vistas dentro de una sola vista raíz.

ContentView y la regla de una sola vista raíz

Da igual si usas Text, Image, Button o cualquier otro componente: SwiftUI necesita que todo esté envuelto en una estructura principal. Las pilas (VStack, HStack, ZStack) cumplen ese rol y además definen cómo se organizan visualmente las vistas hijas.

¿Qué son las pilas (Stacks) en SwiftUI?

Las pilas en SwiftUI son contenedores de layout. Su única responsabilidad es organizar vistas hijas en una dirección concreta.

SwiftUI ofrece tres pilas básicas:

  • VStack → apila vistas de arriba hacia abajo
  • HStack → apila vistas de izquierda a derecha
  • ZStack → superpone vistas en profundidad

Individualmente son simples, pero combinadas permiten construir interfaces sorprendentemente complejas.

En este componente, específicamente en el struct que tiene el mismo nombre del archivo, ContentView por defecto, se encuentra una variable llamada body la cual define el cuerpo de nuestra vista; en la misma tenemos que definir elementos de interfaz gráfica, aunque hay un punto muy importante, y es que SÓLO puede ver un elemento raíz; por ejemplo un texto:

struct ContentView: View {
        
    var body: some View {
        Text("Hola")
            .foregroundColor(blue)
    }
} 
Texto en SwiftUI

Ahora bien, qué pasa si queremos colocar más contenido, qué es lo que seguramente vas a querer hacer en la mayoría de los casos:

struct ContentView: View {
        
    var body: some View {
        Text("Hola")
            .foregroundColor(blue)
        Text("Hola")
            .foregroundColor(blue)
    }
}

Vas a ver que no vas a poder, o no va a funcionar como lo esperado; y aquí es donde entran los componentes del layout.

VStack ,HStack ,ZStack  para apilar o colocar múltiple contenido

Estos vienen siendo los elementos principales en SwiftUI, que vienen siendo los componentes de Layout; son componentes o cajas que nos permiten agrupar múltiples elementos para nuestra Interfaz Gráfica UI, son los famosos 3 componentes de layout VStack, HStack, and ZStack. 

  • VStack es un contenedor que nos permite agregar múltiples hijos, múltiples elementos apilados de manera vertical.
  • HStack es un contenedor que nos permite agregar múltiples hijos, múltiples elementos apilados de manera horizontal.
  • ZStack para colocar los elementos alineándose en cualquiera de sus ejes, lo interesante de esto es que los podemos colocar o definir en cualquier posición, dando una creatividad inmensa para nuestras interfaces.

VStack: apilando vistas de forma vertical

VStack es, probablemente, la pila que más vas a usar. De hecho, en muchos proyectos termina siendo la base de toda la pantalla.

Cuándo usar VStack

Usa VStack cuando quieras mostrar elementos:

  • En forma de columna.
  • Uno debajo del otro.
  • Siguiendo un flujo vertical natural.

Listados, formularios, textos, secciones… casi todo empieza con un VStack.

Así que en resumen, con estos componentes para el layout podemos organizar múltiple contenido, tanto como necesitemos; así que si queremos aplicar textos:

   var body: some View {
        VStack {
            Text("Hola")
                .foregroundColor(.blue)
            Text("Hola")
                .foregroundColor(.red)
            Text("Hola")
                .foregroundColor(.yellow)
        }
    } 
Textos y VStack

Modificadores comunes y orden correcto

Y por supuesto podemos aplicar algunos estilos a nuestro contenidos, como cambiarle el color como hicimos anteriormente o inclusive cambiar el color al elemento contenedor:

     VStack {
            Text("Hola")
                .foregroundColor(.blue)
            Text("Hola")
                .foregroundColor(.red)
            Text("Hola")
                .foregroundColor(.yellow)
        }.background(Color.purple)

Algo de Padding:

        VStack {
            Text("Hola")
                .foregroundColor(.blue)
            Text("Hola")
                .foregroundColor(.red)
            Text("Hola")
                .foregroundColor(.yellow)
        }.padding().background(Color.purple)

Para esto es importante notar que tienes que aplicar el padding antes que el color de fondo, ya que aquí en SwiftUI el orden de los factores SI altera el producto.

Resumen

  • Un VStack agrega elementos de forma vertical, con nuestras vistas o elementos visuales agregadas de arriba a abajo.
  • Agregar el Spacer() obliga a la vista a utilizar la cantidad máxima de espacio vertical.
  • Agregar un Divisor() obliga a la vista a utilizar la cantidad máxima de espacio horizontal.

Spacer y Divider en layouts verticales

  • Spacer() consume todo el espacio disponible en el eje principal.
  • Divider() añade una línea divisoria ocupando solo el espacio necesario.
VStack {
   Text("Arriba")
   Spacer()
   Divider()
   Text("Abajo")
}

Por defecto, la línea divisoria no tiene color. Para establecer el color del divisor, agregamos el modificador/función .background (Color.black). Elementos como background (Color.black), .padding() y .offests(…) se denominan ViewModifiers, o simplemente modificadores. Los modificadores se pueden aplicar a las vistas u otros modificadores, produciendo variaciones.

Recuerda definir el ContentView_Previews para ver un previews del contenido que estás armando.

struct ContentView: View {
        
    var body: some View {
        VStack {
            Text("Hola")
                .foregroundColor(.blue)
            Text("Hola")
                .foregroundColor(.red)
            Text("Hola")
                .foregroundColor(.yellow)
        }.padding().background(Color.purple)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Y ahora, vamos a presentar algunos elementos más que vienen de la mano; por ejemplo, qué pasa si quieres que tu contenedor, VStack, ocupe TODA la pantalla, es decir que crezca a lo largo y ancho y con esto todo el tamaño de la pantalla disponible, para eso, tenemos que emplear los elementos Spacer y Divider respectivamente:

   var body: some View {
        VStack {
            Text("Hola")
                .foregroundColor(.blue)
            Text("Hola")
                .foregroundColor(.red)
            Spacer()
            Divider()
            Text("Hola")
                .foregroundColor(.yellow)
            
        }.padding().background(Color.purple)
    }

HStack: organización horizontal de la interfaz

HStack funciona igual que VStack, pero en horizontal.

Diferencias reales entre VStack y HStack

La lógica es la misma:

  • VStack → eje vertical
  • HStack → eje horizontal

La diferencia práctica aparece cuando usas Spacer y Divider, ya que estos actúan según el eje principal del stack.

Ejemplo práctico con Spacer y Divider

Ahora, si quieres apilar a lo horizontal:

    var body: some View {
        HStack {
            Text("Hola")
                .foregroundColor(.blue)
            Text("Hola")
                .foregroundColor(.red)
            Spacer()
            Divider()
            Text("Hola")
                .foregroundColor(.yellow)
            
        }.padding().background(Color.purple)
    } 
Textos, Spacer, Divider

VStack, apila tu contenido en vertical, HStack apila tu contenido en horizontal

En cualquier caso, también puedes apilar tu contenido de manera vertical, como si fuera una fila, aunque en la mayoría de los casos vas a querer apilarlo en una columna, también puedes aplicarlo de manera vertical, para esto, tenemos que emplear el VStack; por lo demás, todo es exactamente igual al HStack.

Aquí el Spacer empuja el contenido hacia los extremos y el Divider ocupa solo el alto necesario.

Errores comunes en layouts horizontales

Un error típico es esperar que Divider() se expanda automáticamente. En un HStack, el divisor no crece verticalmente si no se lo permites con el layout padre. Son pequeños detalles, pero entenderlos te ahorra mucho tiempo.

ZStack: superposición y profundidad en SwiftUI

ZStack es el stack más flexible… y también el más fácil de usar mal si no se entiende bien.

Cuándo usar ZStack y cuándo no

Usa ZStack cuando necesites:

  • Superponer vistas.
  • Trabajar con capas.
  • Crear efectos de profundidad.

Fondos con texto encima, tarjetas, overlays personalizados… ahí brilla ZStack.

ZStack, pon tus elementos en cualquier parte, en muchos casos, puede que te interese colocar elementos superpuestos o al menos tener más control para personalizar tus elementos en tu interfaz, y esto es lo que te permite hacer el ZStack, que con la función de offset, te permite indicar la posición en el eje cartesiano indicando un parámetro para el x y y:

struct ContentView: View {
    
    var body: some View {
        HStack {
            Text("Hola")
                .foregroundColor(.blue)
            Text("Hola")
                .foregroundColor(.red)
            
            ZStack{
                Text("ZStack Texto 1")
                    .padding()
                    .background(Color.green)
                    .opacity(0.8)
                Divider()
                Text("ZStack Texto 2")
                    .padding()
                    .background(Color.green)
                    .offset(x: 80, y: -200)
            }.background(Color.blue)
            
            Spacer()
            Divider()
            Text("Hola")
                .foregroundColor(.yellow)
        }.padding().background(Color.purple)
    }
} 
ZStack

ZStack vs overlay y background

Algo interesante que explica Apple es que muchas veces no necesitas un ZStack.

Si tienes una vista dominante, overlay o background suelen ser mejores opciones:

Image("ProfilePicture")
   .overlay(Text("Nombre"), alignment: .bottom)

ZStack es ideal cuando el tamaño final depende del conjunto de vistas, no de una sola.

Alineación y espaciado en pilas SwiftUI

SwiftUI separa dos conceptos clave:

  • alineación
  • espaciado

alignment en VStack, HStack y ZStack

  • VStack → controla alineación horizontal
  • HStack → controla alineación vertical
  • ZStack → controla ambos ejes

Importante: la alineación no mueve el stack, mueve las vistas dentro del stack.

Spacer vs Divider: no son lo mismo

  • Spacer expande y empuja
  • Divider separa visualmente

Confundirlos es muy común al principio.

LazyVStack y LazyHStack: rendimiento en listas grandes

Hasta ahora hemos hablado de pilas “normales”. El problema es que cargan todas las vistas a la vez, estén o no en pantalla.

Diferencias entre stacks normales y perezosas

  • VStack / HStack → cargan todo
  • LazyVStack / LazyHStack → cargan bajo demanda

Cuándo usarlas dentro de ScrollView

Si tienes listas largas o contenido dinámico:

ScrollView {
   LazyVStack {
       ForEach(0..<1000) { index in
           Text("Fila \(index)")
       }
   }
}

Aquí SwiftUI solo crea las vistas visibles, mejorando muchísimo el rendimiento y el uso de memoria.

Buenas prácticas para diseñar layouts en SwiftUI

  • Piensa primero en la jerarquía, no en posiciones exactas
  • Divide layouts complejos en stacks pequeños
  • Evita frame y position salvo que sea imprescindible
  • Usa stacks perezosas en contenido scrollable
  • Revisa siempre el orden de los modificadores

Preguntas frecuentes sobre VStack, HStack y ZStack

  • ¿Por qué no puedo poner varios Text en body?
    • Porque SwiftUI solo permite una vista raíz. Necesitas un contenedor como VStack.
  • ¿Cuál stack debo usar primero?
    • En la mayoría de pantallas, VStack suele ser el punto de partida.
  • ¿ZStack reemplaza a overlay?
    • No. overlay es mejor cuando hay una vista dominante; ZStack cuando el tamaño depende del conjunto.
  • ¿Cuándo usar LazyVStack?
    • Cuando trabajes con listas largas o contenido dentro de un ScrollView.
  • ¿El orden de los modificadores importa?
    • Sí. Mucho. En SwiftUI, el orden define el resultado visual.

Conclusiones

Ya con esto tienes los elementos necesarios para emplear a crear tus primeras interfaces y empezar a organizar tus interfaces como tu quieras. 

Estos elementos contenedores, son fundamentales para construir las bases de tu app, para indicar la estructura en la cual van a estar contenidos otros elementos como imágenes, textos, campos de texto, elementos de formulario etc.

Las pilas son la base del layout en SwiftUI. Entender VStack, HStack y ZStack no es solo aprender tres componentes, sino cambiar la forma de pensar la interfaz.

En cuanto asimilas la regla de una sola vista raíz y empiezas a usar stacks como contenedores mentales, todo encaja: los errores desaparecen, el código se simplifica y los layouts se vuelven más predecibles.

Acepto recibir anuncios de interes sobre este Blog.

Aprende qué son las pilas en SwiftUI y cómo usar VStack, HStack y ZStack para crear layouts adaptables, eficientes y bien estructurados.

| 👤 Andrés Cruz

🇺🇸 In english