Saltar al contenido
K
iOS con SwiftUIPaso a paso27 min de lectura · 6 min de máquina Intermedio

@State

Al terminar: Usar @State para que la vista se redibuje cuando cambia un valor, y compararlo con remember de Compose.

Al terminar esta lección, la tarjeta de Marcador lee su resultado de un dato que guarda la propia pantalla (PantallaMarcador) y arranca en 0 - 0. Los botones de gol todavía no hacen nada: los conectas en la lección siguiente, @Binding. Antes de esto, la pantalla dibujaba un resultado fijo.

Importa porque en SwiftUI casi toda la interfaz es esto: alguien guarda un dato y la pantalla se redibuja cuando cambia. Si no sabes dónde vive el dato, o no cambia o cambia en una copia que nadie ve.

Trabajas en ~/Marcador-Swift/MarcadorApp. Los ficheros nuevos se crean con clic derecho sobre la carpeta que te indique cada paso (dentro de MarcadorApp, la que contiene MarcadorApp.swift) → New File from Template… → Swift File → Next, el nombre y Create. Si el menú de tu versión de Xcode se llama distinto, usa la Terminal: touch del fichero dentro de ~/Marcador-Swift/MarcadorApp/MarcadorApp/<carpeta>/, y Xcode lo muestra solo.

Una vista no puede guardar nada

Una vista de SwiftUI es un struct: un valor, y self dentro de body es inmutable (no se puede cambiar). Además SwiftUI crea y tira estos structs todo el rato. Si guardas un dato en una propiedad normal, no se puede ni cambiar ni recordar.

@State resuelve las dos cosas. Es un property wrapper (una anotación que envuelve una propiedad y le añade comportamiento; lo viste de pasada en El JSON de Kotlin en Swift: recursos y errores de lectura). @State saca el valor del struct y lo guarda en un almacén que gestiona SwiftUI. Cuando el valor cambia, SwiftUI vuelve a pedir el body de esa vista, y la pantalla se actualiza.

Dicho sin tecnicismos: Swift te da gratis una forma de crear un struct pasando sus propiedades (Contador(goles: 5)). Con private, esa puerta se cierra para el @State y nadie le impone un valor desde fuera.

ejemplo mínimo: un contador con @State (con comentarios)
struct Contador: View {
    // `@State`: el dato vive fuera del struct, en el almacén de SwiftUI.
    // `private`: esta vista es su dueña y nadie más lo toca.
    @State private var goles = 0
 
    var body: some View {
        // El texto del botón lee el dato: cada vez que cambia, SwiftUI pide un `body` nuevo.
        Button("Goles: \(goles)") {
            goles += 1                        // cambia el estado: SwiftUI repinta el botón
        }
    }
}

Al empezar el botón dice «Goles: 0»; cada toque suma uno y el texto se actualiza solo:

Goles: 0      (al empezar)
Goles: 3      (después de tocar tres veces)

Qué pasa si no lo usas

Si el dato que cambia no es @State, no compila. Es el primer fallo que verás: el texto es este o muy parecido (varía un poco entre versiones de Xcode).

con el bug: una propiedad normal no se puede cambiar
struct Pantalla: View {
    var goles = 0
 
    var body: some View {
        Button("Gol \(goles)") {
            goles += 1
        }
    }
}
error: left side of mutating operator isn't mutable: 'self' is immutable
corregido: @State para el dato que cambia
struct Pantalla: View {
    @State private var goles = 0
 
    var body: some View {
        Button("Gol \(goles)") {
            goles += 1
        }
    }
}

Falla porque body no puede modificar self: la propiedad goles es parte del struct, y el struct es inmutable. Con @State, el dato ya no está en el struct.

ComposeSwiftUI
var goles by remember { mutableStateOf(0) }@State private var goles = 0
Al cambiar el valor, se recompone la funciónAl cambiar el valor, se vuelve a pedir el body

Reto · Ejercicio: el contador de cafés

Los ejercicios de SwiftUI se hacen dentro del proyecto, en la carpeta Ejercicios, con una subcarpeta por ejercicio (la creaste en El ciclo de vida de UIViewController). Crea una subcarpeta nueva dentro de Ejercicios, ContadorDeCafes, y dentro un fichero ContadorDeCafes.swift. Los ejercicios no se borran y todo lo que pongas ahí compila con la app, así que cada uno usa nombres propios.

Escribe una vista, ContadorDeCafes, que es la dueña de cuántos cafés ha servido una cafetería. Muestra «Cafés: 0» en tipografía grande y negrita y, debajo, dos botones destacados en fila: «Un café más» suma uno y «Empezar de cero» lo vuelve a 0. Añade al final del fichero una vista previa que la muestre.

En la vista previa del lienzo (Editor → Canvas; la vista previa se puede tocar como la app), debe verse:

  • Al empezar, «Cafés: 0».
  • Tras tocar tres veces «Un café más», «Cafés: 3».
  • Tras tocar «Empezar de cero», «Cafés: 0».
Ver solución
Ejercicios/ContadorDeCafes/ContadorDeCafes.swift
import SwiftUI
 
// Cuenta los cafés que sirve una cafetería. La vista es la dueña del dato.
struct ContadorDeCafes: View {
    @State private var cafes = 0
 
    var body: some View {
        VStack(spacing: 16) {
            Text("Cafés: \(cafes)")
                .font(.largeTitle.bold())
            HStack(spacing: 8) {
                Button("Un café más") { cafes += 1 }
                Button("Empezar de cero") { cafes = 0 }
            }
            .buttonStyle(.borderedProminent)
        }
    }
}
 
#Preview {
    ContadorDeCafes()
}

Los botones destacados son .buttonStyle(.borderedProminent), el mismo de los botones de gol. Si el lienzo de tu Xcode no responde a los toques, comprueba al menos que el fichero compila con Cmd+B.

Paso 1 · Renombra la pantalla

Objetivo: que PantallaMarcadorEstatica pase a llamarse PantallaMarcador: ya no es estática, y las lecciones siguientes usan ese nombre. Compila igual que antes.

MarcadorApp
MarcadorApp
Marcador
UI
PantallaMarcador.swiftse renombra desde PantallaMarcadorEstatica.swift
ContentView.swiftse modifica
Un fichero se renombra y otro se modifica.
  1. En el Project navigator, haz clic en PantallaMarcadorEstatica.swift (dentro de Marcador/UI) para seleccionarlo, pulsa Return y escribe PantallaMarcador.swift.
  2. Dentro del fichero, cambia el nombre del struct en dos sitios: struct PantallaMarcadorEstatica: View pasa a struct PantallaMarcador: View, y PantallaMarcadorEstatica() dentro de #Preview pasa a PantallaMarcador().
  3. En ContentView.swift, la primera pestaña pasa a llamar a PantallaMarcador() en lugar de PantallaMarcadorEstatica().

Cmd+B: Build Succeeded. Si algún nombre antiguo se te escapa, Xcode lo marca con cannot find 'PantallaMarcadorEstatica' in scope.

Paso 2 · @State en la pantalla del marcador

Objetivo: que la tarjeta lea el marcador de un dato guardado en PantallaMarcador, que arranca en 0 - 0. Los botones todavía no lo cambian.

Piezas nuevas de este paso:

  • @State private var marcador = Marcador(local: 0, visitante: 0): el dato y su dueña. Marcador es del paquete Torneo: un valor con los goles de cada lado.
  • marcador.local y marcador.visitante: los goles de cada lado; la tarjeta los pinta en lugar de leerlos del partido.

Dos cambios. En PantallaMarcador, añade el estado justo debajo del partido y pásaselo a la tarjeta (las líneas que no se tocan se resumen en …):

PantallaMarcador.swift: el estado y la tarjeta que lo recibe
struct PantallaMarcador: View {
    let partido: Partido = Ejemplo.rayoContraToros
 
    // El marcador que cambiará con los botones. `private`: su dueña es esta vista y nadie más lo toca.
    @State private var marcador = Marcador(local: 0, visitante: 0)
 
    var body: some View {
        VStack(spacing: 24) {
            // …el título, sin cambios…
 
            // La tarjeta recibe ahora el marcador, además del partido.
            TarjetaDePartido(partido: partido, marcador: marcador)
 
            // …los botones y el resto, sin cambios…
        }
        .padding(16)
    }
}

Y TarjetaDePartido recibe el Marcador en lugar de leer los goles del partido. Sustituye TarjetaDePartido entera:

PantallaMarcador.swift: TarjetaDePartido con marcador
struct TarjetaDePartido: View {
    let partido: Partido        // para los nombres de los equipos
    let marcador: Marcador      // los goles, que ahora vienen de fuera
 
    var body: some View {
        HStack {
            Text(partido.local.nombre)
                .font(.title3)
                .frame(maxWidth: .infinity, alignment: .leading)
            // Los goles salen del `Marcador` que recibe la tarjeta.
            Text("\(marcador.local) - \(marcador.visitante)")
                .font(.system(size: 40, weight: .bold, design: .rounded))
                .monospacedDigit()
            Text(partido.visitante.nombre)
                .font(.title3)
                .frame(maxWidth: .infinity, alignment: .trailing)
        }
        .padding(20)
        .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
    }
}

Ejecuta con Cmd+R: la tarjeta arranca en 0 - 0, y los botones todavía no hacen nada. Los datos fijos del partido de ejemplo (2-1) ya no se ven.

La prueba de arranque

La prueba de interfaz que ya tienes busca 2 - 1 al arrancar, y ahora la tarjeta dice 0 - 0: fallaría. Cambia un solo texto. Abre MarcadorAppUITests → MarcadorAppUITests.swift, busca testArrancaYMuestraElMarcador y sustituye 2 - 1 por 0 - 0 en la línea de en medio de estas tres (las otras dos ya las tienes, no las toques):

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
MarcadorAppUITests.swift: la prueba de arranque, con 0 - 0
        XCTAssertTrue(app.staticTexts["Rayo FC"].waitForExistence(timeout: 5))
        XCTAssertTrue(app.staticTexts["0 - 0"].exists)    // antes decía "2 - 1"
        capturar(app, "marcador")

Pasa todas las pruebas con Cmd+U, o desde la Terminal:

cd ~/Marcador-Swift/MarcadorApp
xcodebuild -scheme MarcadorApp -destination 'platform=iOS Simulator,name=iPhone 17' build test

El nombre del simulador tiene que existir en tu Mac; con xcrun simctl list devices available ves los que tienes.

Punto de control

  • En la pestaña «Marcador» la tarjeta arranca en 0 - 0 y los botones de gol todavía no hacen nada.
  • Las pruebas pasan, y la orden termina en ** TEST SUCCEEDED ** (con el simulador que tengas: cambia iPhone 17 por un nombre de xcrun simctl list devices available): cinco pruebas, una unitaria y cuatro de interfaz, las mismas de antes con la de arranque cambiada.
  • El proyecto tiene Marcador/UI/PantallaMarcador.swift (con PantallaMarcador y TarjetaDePartido) y ya no existe PantallaMarcadorEstatica.swift. En Ejercicios tienes ContadorDeCafes.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Qué hace @State private var marcador = Marcador(local: 0, visitante: 0) en una vista?

  2. 2.Escribes var goles = 0 en una vista y goles += 1 dentro de un botón. ¿Qué pasa?

  3. 3.¿Por qué conviene declarar un @State como private?

  4. 4.¿Por qué un struct de vista no puede guardar un dato que cambia en una propiedad normal?

  5. 5.¿Qué se parece más a @State private var goles = 0 de SwiftUI en Compose?

Faltan 5 de 5