Programación en Swift y SwiftUI para iOS Developers

ZStack en SwiftUI explicado con ejemplos

Introducción: Pensando más allá de las Filas y Columnas

Cuando empezamos a diseñar interfaces en SwiftUI (o en cualquier framework de UI moderno), nuestro cerebro tiende a pensar en dos dimensiones: vertical y horizontal. “Quiero un título, y debajo del título una imagen, y al lado de la imagen un texto”. Para esto, herramientas como VStack (Vertical Stack) y HStack (Horizontal Stack) son nuestros mejores amigos.

Pero, ¿qué pasa cuando el diseño requiere algo más complejo? ¿Qué sucede cuando necesitas poner un texto encima de una imagen, crear un botón flotante que se quede fijo en una esquina sobre una lista, o diseñar una tarjeta de crédito realista con un fondo degradado y elementos superpuestos?

Aquí es donde entramos en la tercera dimensión del diseño de interfaces. Aquí es donde entra ZStack.

En este tutorial de 2000 palabras, vamos a diseccionar el componente ZStack de SwiftUI. No solo aprenderás su sintaxis, sino que entenderás cómo piensa el motor de renderizado de Apple, cómo evitar errores comunes de diseño y cómo crear interfaces profesionales capa por capa.


Parte 1: ¿Qué es exactamente un ZStack?

El nombre ZStack proviene del Eje Z en la geometría cartesiana.

  • Eje X: Izquierda / Derecha (HStack).
  • Eje Y: Arriba / Abajo (VStack).
  • Eje Z: Adelante / Atrás (Profundidad).

Imagina que tu escritorio es la pantalla de tu iPhone.

  1. Si pones una hoja de papel sobre la mesa, esa es tu primera vista.
  2. Si pones una fotografía encima de esa hoja, la fotografía tapa parcialmente el papel.
  3. Si pones una moneda sobre la fotografía, la moneda está en la capa superior.

Eso es un ZStack. Es un contenedor que apila sus vistas hijas una encima de la otra, alineándolas en el centro por defecto.

La Regla de Oro del Orden

En el código de SwiftUI, el orden de lectura es crucial.

En un ZStack, la primera vista que escribes es la que está al fondo (más lejos del usuario). La última vista que escribes es la que está al frente (más cerca del usuario).


Parte 2: Tu Primer ZStack (Conceptos Básicos)

Vamos a abrir Xcode y ver esto en acción. Empezaremos con algo muy básico para visualizar las capas.

import SwiftUI

struct ConceptoBasicoView: View {
    var body: some View {
        ZStack {
            // Capa 1: El Fondo (Al fondo del todo)
            Color.blue
                .frame(width: 300, height: 300)
                .cornerRadius(20)
            
            // Capa 2: El elemento intermedio
            Circle()
                .fill(Color.yellow)
                .frame(width: 200, height: 200)
            
            // Capa 3: El frente (Lo más cercano al usuario)
            Text("Hola ZStack")
                .font(.largeTitle)
                .fontWeight(.bold)
                .foregroundColor(.white)
        }
    }
}

Análisis del Código:

  1. Color.blue: Actúa como nuestra base. Nota cómo SwiftUI trata los colores como Vistas.
  2. Circle: Se dibuja encima del cuadrado azul.
  3. Text: Se dibuja encima del círculo amarillo.

Si cambiaras el orden y pusieras el Color.blue al final del código, este taparía al círculo y al texto, y solo verías un cuadrado azul.


Parte 3: El Poder de la Alineación (Alignment)

A diferencia de VStack (que alinea horizontalmente) o HStack (que alinea verticalmente), el ZStack tiene un sistema de alineación bidimensional mucho más potente.

Por defecto, todo se centra. Pero, ¿y si quieres colocar elementos en las esquinas?

El inicializador de ZStack acepta un parámetro alignment.

ZStack(alignment: .topLeading) { // Alineación arriba a la izquierda
    Color.red.frame(width: 300, height: 300)
    
    Text("Esquina Superior")
        .foregroundColor(.white)
        .padding()
}

El problema de la alineación global

El parámetro alignment del ZStack afecta a todas las vistas dentro de él. Si pones .topLeadingtodas las capas intentarán irse a la esquina superior izquierda.

¿Cómo logramos que una capa esté en el centro y otra en la esquina? Usando Frame con maxWidth y maxHeight o Spacersdentro de las capas individuales, no en el contenedor.

Sin embargo, la forma más limpia en SwiftUI moderno es usar el modificador .frame(maxWidth: .infinity, alignment: ...) en la vista hija, o usar otro contenedor dentro del ZStack.

Veamos un ejemplo práctico: Una tarjeta de perfil.

struct TarjetaPerfilView: View {
    var body: some View {
        ZStack(alignment: .bottom) { // Alineación por defecto abajo
            // 1. Imagen de fondo
            Image("paisaje") // Asegúrate de tener una imagen en Assets
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: 300, height: 200)
                .clipped()
            
            // 2. Degradado para que el texto se lea mejor
            LinearGradient(colors: [.black.opacity(0.6), .clear],
                           startPoint: .bottom,
                           endPoint: .top)
                .frame(width: 300, height: 200)
            
            // 3. Texto (Alineado abajo gracias al ZStack)
            HStack {
                Text("Montañas Rocosas")
                    .foregroundColor(.white)
                    .font(.headline)
                Spacer()
                Image(systemName: "heart.fill")
                    .foregroundColor(.red)
            }
            .padding()
            .frame(width: 300) // Coincide con el ancho de la imagen
        }
        .cornerRadius(15)
        .shadow(radius: 5)
    }
}

Parte 4: ZIndex – Rompiendo el Orden Natural

A veces, la lógica de tu aplicación requiere que cambies el orden de las capas dinámicamente, sin cambiar el orden de las líneas de código. O quizás estás usando animaciones donde una vista debe pasar por encima de otra.

Para esto existe el modificador .zIndex().

Por defecto, todas las vistas tienen un índice Z de 0. SwiftUI las dibuja en orden de aparición. Pero si asignas un valor manual, tomas el control.

struct BarajaCartasView: View {
    @State private var cartaSeleccionada = false

    var body: some View {
        ZStack {
            // Carta Roja
            Rectangle()
                .fill(Color.red)
                .frame(width: 200, height: 300)
                .rotationEffect(.degrees(10))
                .zIndex(cartaSeleccionada ? 1 : 2) // Si seleccionada, va atrás
                .onTapGesture { withAnimation { cartaSeleccionada.toggle() } }

            // Carta Azul
            Rectangle()
                .fill(Color.blue)
                .frame(width: 200, height: 300)
                .rotationEffect(.degrees(-10))
                .zIndex(cartaSeleccionada ? 2 : 1) // Si seleccionada, va al frente
                .onTapGesture { withAnimation { cartaSeleccionada.toggle() } }
        }
    }
}

Nota Importante: El .zIndex es fundamental cuando trabajas con transiciones (aparecer/desaparecer vistas). Si SwiftUI no sabe qué vista está “encima” de la otra durante una animación, el resultado visual puede ser extraño (vistas que se cortan entre sí).


Parte 5: Casos de Uso del Mundo Real

Suficiente teoría. Vamos a construir cosas que verías en una aplicación real de la App Store.

Caso 1: El “Floating Action Button” (FAB)

Muy común en apps como Twitter o Gmail. Una lista de contenido y un botón fijo en la esquina inferior derecha.

struct ListaConFABView: View {
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            // Capa 1: El Contenido Principal
            List(0..<50) { i in
                Text("Elemento de lista #\(i)")
            }
            
            // Capa 2: El Botón Flotante
            Button(action: {
                print("Crear nuevo elemento")
            }) {
                Image(systemName: "plus")
                    .font(.title)
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.blue)
                    .clipShape(Circle())
                    .shadow(radius: 4, y: 4)
            }
            .padding() // Margen para separarlo del borde de la pantalla
        }
    }
}

Este es el patrón clásico: ZStack como contenedor raíz de la pantalla. La lista ocupa todo el espacio, y el botón se coloca encima, alineado a la esquina.

Caso 2: Pantalla de “Cargando” (Loading Overlay)

¿Cómo bloqueas la pantalla cuando estás haciendo una llamada a una API? Con un ZStack.

struct LoadingViewExample: View {
    @State private var isLoading = false

    var body: some View {
        ZStack {
            // 1. Tu interfaz normal
            VStack {
                Text("Bienvenido a mi App")
                Button("Cargar Datos") {
                    isLoading = true
                    // Simular carga
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                        isLoading = false
                    }
                }
            }
            
            // 2. Capa de bloqueo (Solo aparece si isLoading es true)
            if isLoading {
                Color.black.opacity(0.4)
                    .ignoresSafeArea() // Cubre TODA la pantalla, incluso la barra de estado
                
                ProgressView("Cargando...")
                    .padding()
                    .background(Color.white)
                    .cornerRadius(10)
                    .shadow(radius: 10)
            }
        }
        .animation(.easeInOut, value: isLoading) // Animación suave al aparecer
    }
}

Este ejemplo demuestra el poder de combinar lógica (if) con diseño (ZStack). La capa semitransparente impide que el usuario toque los botones de abajo mientras carga.


Parte 6: ZStack vs .overlay() vs .background()

Una duda muy común en desarrolladores intermedios es: ¿Cuándo uso ZStack y cuándo uso el modificador .overlay?

Visualmente, el resultado puede ser idéntico, pero semánticamente son distintos.

1. .background()

Úsalo cuando la capa inferior es puramente decorativa y depende del tamaño del contenido.

  • Ejemplo: Un color de fondo en un botón.

2. .overlay()

Úsalo cuando el elemento superior es un accesorio o decoración del elemento principal. El tamaño del overlay está limitado por el tamaño de la vista padre.

  • Ejemplo: Un badge de notificación (ese círculo rojo con un número) sobre un icono, o un borde sobre una imagen.
// Ejemplo de Overlay
Image(systemName: "bell.fill")
    .font(.largeTitle)
    .overlay(alignment: .topTrailing) {
        Circle()
            .fill(.red)
            .frame(width: 12, height: 12)
            .offset(x: 2, y: -2)
    }

3. ZStack

Úsalo cuando tienes vistas hermanas independientes que comparten el mismo espacio, o cuando necesitas controlar la alineación de múltiples elementos de forma compleja.

  • Ejemplo: Un fondo de pantalla completo con elementos encima, o un diseño de capas tipo Photoshop.

Regla práctica: Si estás anidando demasiados .overlay dentro de .overlay, probablemente deberías cambiar a un ZStack.


Parte 7: El problema del “Safe Area” y ZStack

Un error clásico al usar ZStack para fondos es que el color no llega hasta los bordes del notch (la muesca del iPhone) o la barra de inicio en la parte inferior.

Esto sucede porque, por defecto, SwiftUI respeta el “Safe Area” (Área Segura) para que no dibujes contenido donde el usuario no puede verlo bien.

Para fondos en un ZStack, casi siempre querrás ignorarlo:

ZStack {
    Color.purple
        .ignoresSafeArea() // ¡Crucial para fondos!
    
    VStack {
        Text("Contenido Seguro")
        // El contenido aquí SÍ respeta los márgenes, 
        // porque el ignoreSafeArea se aplicó solo al Color, no al ZStack entero.
    }
}

Si aplicas .ignoresSafeArea() al ZStack completo, tu texto podría quedar tapado por el notch. Aplícalo solo a la capa del fondo.


Parte 8: Optimización y Renderizado

Aunque SwiftUI es muy eficiente, el abuso de ZStacks con cientos de capas complejas puede afectar el rendimiento, especialmente si aplicas sombras o desenfoques (blur) a cada capa.

LazyZStack… ¿Existe?

A diferencia de VStack y HStack, que tienen sus versiones Lazy (LazyVStack) para cargar contenido bajo demanda (útil en listas infinitas), no existe un LazyZStack.

¿Por qué? Porque en un eje Z, normalmente necesitas ver todas las capas para componer la imagen final. No puedes hacer “scroll” en profundidad de la misma manera que en vertical. Por lo tanto, ten cuidado al usar ZStack para apilar cientos de vistas; todas se cargarán en memoria inmediatamente.


Conclusión

El ZStack es la herramienta que separa a los diseños novatos de las interfaces profesionales en SwiftUI. Te permite escapar de la rigidez de las listas y las cuadrículas para crear experiencias inmersivas, interfaces modulares y efectos visuales complejos.

Resumen de puntos clave:

  1. Orden: Código arriba = Fondo. Código abajo = Frente.
  2. Alineación: Usa alignment en el ZStack o Spacers y Frames en los hijos.
  3. Uso: Ideal para fondos, elementos flotantes y estados de carga.
  4. Alternativas: Usa .overlay para decoraciones pequeñas ligadas a una vista padre.

¿Cuál es tu siguiente paso?

Abre Xcode ahora mismo. Intenta replicar la interfaz de tu aplicación de música favorita. Verás que esa carátula del álbum con el texto encima y los controles flotantes… todo eso, es un ZStack esperando a ser escrito.

Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Cómo crear una app para el Apple Watch

Next Article

Qué es y cómo usar Label en SwiftUI

Related Posts