El estado en SwiftUI es la información que tu app recuerda en cada momento, y la navegación es el mecanismo que lleva al usuario de una pantalla a otra.
¿Qué es el estado en una app iOS?
El estado es cualquier dato que determina qué se muestra en pantalla. Cuando el estado cambia, SwiftUI redibuja la interfaz automáticamente.
Por ejemplo, si el usuario escribe su nombre o selecciona un producto de Liverpool, esos valores son estado. Tu app necesita recordarlos para mostrarlos en otra pantalla.
@State: la variable que SwiftUI vigila
@State es un property wrapper que le dice a SwiftUI: "cuando este valor cambie, redibuja esta vista".
Solo úsalo para datos locales a una vista, como un contador, un texto ingresado o un interruptor activado.
Estructura básica
@State private var nombre: String = ""
@State private var precio: Double = 0.0
@State private var esMiembro: Bool = false
Ejemplo 1 — Contador de artículos en carrito
Imagina una app sencilla para una tienda como Mercado Libre:
struct CarritoView: View {
@State private var cantidad: Int = 0
var body: some View {
VStack(spacing: 20) {
Text("Artículos en carrito: \(cantidad)")
.font(.title2)
HStack(spacing: 16) {
Button("−") {
if cantidad > 0 { cantidad -= 1 }
}
.buttonStyle(.bordered)
Button("+") {
cantidad += 1
}
.buttonStyle(.borderedProminent)
}
}
.padding()
}
}
Cada vez que el usuario toca "+" o "−", cantidad cambia y SwiftUI redibuja Text con el nuevo número. No necesitas código extra para actualizar la interfaz.
@Binding: compartir estado entre vistas
@Binding permite que una vista hija lea y modifique el estado que vive en la vista padre.
No crea datos nuevos. Solo apunta a los datos que ya existen en otra vista.
Estructura básica
// Vista padre — tiene @State
@State private var estaActivo: Bool = false
// Vista hija — recibe @Binding
struct MiToggle: View {
@Binding var estaActivo: Bool
...
}
// Llamada con el signo $
MiToggle(estaActivo: $estaActivo)
El signo $ convierte una variable @State en un Binding. Sin él, solo pasas el valor actual, no la conexión en dos direcciones.
Ejemplo 2 — Formulario de registro con vista separada
Supón que tienes una app de puntos para socios de FEMSA. El formulario está dividido en dos vistas:
struct CampoNombre: View {
@Binding var nombre: String
var body: some View {
TextField("Escribe tu nombre", text: $nombre)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
}
}
struct RegistroView: View {
@State private var nombreSocio: String = ""
var body: some View {
VStack(spacing: 16) {
Text("Registro de socio FEMSA")
.font(.headline)
CampoNombre(nombre: $nombreSocio)
Text("Hola, \(nombreSocio.isEmpty ? "nuevo socio" : nombreSocio)")
.foregroundStyle(.secondary)
}
.padding()
}
}
CampoNombre no necesita saber nada sobre RegistroView. Solo recibe el binding y lo usa. Cuando el usuario escribe, nombreSocio en RegistroView se actualiza en tiempo real.
NavigationStack: conectar pantallas
NavigationStack es el contenedor que habilita la navegación entre vistas usando una pila (stack). La pantalla nueva se apila sobre la anterior y el usuario puede regresar con el botón atrás.
Estructura básica
NavigationStack {
// Vista raíz
NavigationLink("Ir a detalles", destination: DetalleView())
}
NavigationLink es el botón que empuja una nueva vista a la pila. El parámetro destination recibe la vista a la que quieres navegar.
Ejemplo 3 — App de dos pantallas: lista de productos Bimbo
Esta app muestra una lista de productos y al tocar uno abre el detalle con su precio.
struct Producto: Identifiable {
let id = UUID()
let nombre: String
let precio: Double
}
let productosBimbo: [Producto] = [
Producto(nombre: "Pan Blanco", precio: 42.5),
Producto(nombre: "Marinela Gansito", precio: 18.5),
Producto(nombre: "Tía Rosa Polvorón", precio: 29.0)
]
struct ListaProductosView: View {
var body: some View {
NavigationStack {
List(productosBimbo) { producto in
NavigationLink(destination: DetalleProductoView(producto: producto)) {
Text(producto.nombre)
}
}
.navigationTitle("Productos Bimbo")
}
}
}
struct DetalleProductoView: View {
let producto: Producto
var body: some View {
VStack(spacing: 12) {
Text(producto.nombre)
.font(.largeTitle)
.bold()
Text("Precio: $\(String(format: "%.0f", producto.precio))")
.font(.title3)
.foregroundStyle(.secondary)
Spacer()
}
.padding()
.navigationTitle("Detalle")
.navigationBarTitleDisplayMode(.inline)
}
}
Cuando el usuario toca "Pan Blanco", NavigationStack empuja DetalleProductoView con ese producto. El botón atrás aparece automáticamente en la barra de navegación.
Estado + navegación juntos
Puedes combinar @State y NavigationStack para controlar la navegación desde código, no solo desde botones.
Ejemplo 4 — Navegación programática con navigationDestination
struct BuscadorView: View {
@State private var busqueda: String = ""
@State private var irAResultados: Bool = false
var body: some View {
NavigationStack {
VStack(spacing: 20) {
TextField("Busca un producto", text: $busqueda)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Button("Buscar") {
irAResultados = true
}
.buttonStyle(.borderedProminent)
.disabled(busqueda.isEmpty)
}
.navigationTitle("Buscador")
.navigationDestination(isPresented: $irAResultados) {
ResultadosView(termino: busqueda)
}
}
}
}
struct ResultadosView: View {
let termino: String
var body: some View {
Text("Resultados para: \"\(termino)\"")
.font(.title2)
.padding()
.navigationTitle("Resultados")
}
}
.navigationDestination(isPresented:) navega cuando irAResultados se vuelve true. Así controlas la navegación desde una acción como tocar "Buscar", no solo desde un NavigationLink visible.
Errores comunes
Error 1 — Usar @State para datos compartidos entre vistas no relacionadas.
@State es solo para datos locales de una vista. Si varias vistas necesitan el mismo dato, usa @Binding o, en apps más complejas, @StateObject con una clase ObservableObject.
Error 2 — Olvidar el signo $ al pasar un Binding.
Escribir CampoNombre(nombre: nombreSocio) pasa una copia del valor, no la conexión. La vista hija no podrá actualizar el dato original. Siempre usa $nombreSocio cuando el parámetro espera un Binding.
Error 3 — Colocar NavigationStack dentro de otra vista que ya tiene NavigationStack.
Anidar dos NavigationStack produce comportamientos inesperados: barras de navegación duplicadas o botones atrás que no funcionan. Define un solo NavigationStack en la vista raíz de cada flujo.
Error 4 — Modificar variables @State fuera del hilo principal.
Si obtienes datos de una API (por ejemplo, precios del SAT o inventario de Liverpool), actualiza las variables @State siempre en el hilo principal con await MainActor.run { } o marcando la función con @MainActor. De lo contrario, la interfaz puede no actualizarse correctamente.
Tabla de referencia rápida
| Herramienta | ¿Dónde vive el dato? | ¿Para qué sirve? |
|---|---|---|
@State |
En la vista actual | Variables locales que cambian con interacción |
@Binding |
En la vista padre | Compartir y modificar estado desde una vista hija |
NavigationStack |
Vista contenedora | Habilita navegación con pila entre pantallas |
NavigationLink |
Dentro del stack | Botón que empuja una nueva vista |
.navigationDestination |
Modificador de vista | Navegación programática desde código |
Puntos clave
@Statealmacena datos locales de una vista. Cuando cambia, SwiftUI redibujabodyautomáticamente.@Bindingconecta una vista hija con el@Statede su padre. El signo$es obligatorio para crear esa conexión.NavigationStackes el contenedor que habilita la navegación por pila. Defínelo una sola vez por flujo.NavigationLinky.navigationDestinationson las dos formas de navegar: la primera desde un elemento visible, la segunda desde una condición en el código.- Combinar
@Statecon.navigationDestination(isPresented:)te permite navegar cuando ocurre una acción, no solo cuando el usuario toca un botón específico.