tutoriales.com

Gestionando el Estado de la Aplicación en SwiftUI con Patrones Avanzados

Este tutorial exhaustivo explora las estrategias y herramientas clave para una gestión de estado eficiente en SwiftUI. Aprenderás a utilizar las Property Wrappers de SwiftUI, integrar Combine para flujos de datos reactivos y aplicar patrones avanzados para crear aplicaciones escalables y fáciles de mantener. Ideal para desarrolladores de Swift que buscan mejorar sus habilidades en SwiftUI.

Intermedio20 min de lectura22 views15 de marzo de 2026Reportar error

La gestión del estado es el corazón de cualquier aplicación moderna. En SwiftUI, Apple nos ha proporcionado un conjunto robusto de herramientas para manejar el estado, pero dominar su uso y saber cuándo aplicar cada una puede ser un desafío. Este tutorial te guiará a través de los conceptos fundamentales y avanzados para construir arquitecturas de estado sólidas y reactivas en tus aplicaciones SwiftUI.


🚀 Introducción a la Gestión de Estado en SwiftUI

SwiftUI es un framework declarativo. Esto significa que describes cómo debe verse tu interfaz de usuario en función del estado actual de tu aplicación, y SwiftUI se encarga de actualizar la vista cuando ese estado cambia. Comprender cómo y dónde almacenar tu estado es crucial para crear aplicaciones eficientes, predecibles y fáciles de depurar.

¿Qué es el "Estado" en una Aplicación?

El estado se refiere a cualquier dato que puede cambiar con el tiempo y que afecta la apariencia o el comportamiento de tu interfaz de usuario. Puede ser tan simple como un booleano que controla la visibilidad de un elemento, o tan complejo como una lista de objetos que representan datos de una API.

💡 Consejo: Piensa en el estado como la "verdad" de tu aplicación. Si el estado cambia, la interfaz de usuario debe reflejar ese cambio.

Los Pilares de la Reactividad en SwiftUI

SwiftUI utiliza un sistema de reactividad para responder a los cambios de estado. Esto se logra mediante Property Wrappers especiales que marcan las propiedades para que SwiftUI las "observe". Cuando una propiedad marcada cambia, SwiftUI invalida la vista correspondiente y la redibuja con el nuevo estado.


✨ Property Wrappers Fundamentales para el Estado Local

SwiftUI nos ofrece varias Property Wrappers para gestionar diferentes tipos de estado. Comenzaremos con las más comunes para el estado local de una vista.

@State: El Estado Básico de una Vista

@State es la Property Wrapper más fundamental y se utiliza para almacenar datos simples que pertenecen y son gestionados por una única vista. Cuando una propiedad @State cambia, la vista que la posee se invalida y se redibuja.

struct ContadorView: View {
    @State private var count: Int = 0

    var body: some View {
        VStack {
            Text("Contador: \(count)")
                .font(.largeTitle)
            Button("Incrementar") {
                count += 1
            }
        }
    }
}
📌 Nota: Es una buena práctica declarar las propiedades `@State` como `private` para enfatizar que son de uso interno de la vista.

@Binding: Compartiendo Estado Bidireccional

@Binding permite que una vista secundaria acceda y modifique una propiedad @State (u otra propiedad enlazable) de su vista padre sin poseer directamente el estado. Esto es crucial para la reutilización de componentes.

struct BotonContadorView: View {
    @Binding var count: Int

    var body: some View {
        Button("Incrementar desde Botón") {
            count += 1
        }
    }
}

struct ContenedorView: View {
    @State private var valorActual: Int = 0

    var body: some View {
        VStack {
            Text("Valor: \(valorActual)")
            BotonContadorView(count: $valorActual) // Pasamos el binding
        }
    }
}

Observa el signo $ antes de valorActual al pasar el Binding. Esto crea un enlace a la propiedad.


🛠️ Gestión del Estado con Referencia: @ObservedObject y @StateObject

Para estados más complejos o que necesitan ser compartidos entre múltiples vistas o persistir a través de recreaciones de vistas, necesitamos objetos de referencia.

ObservableObject y @Published

Para que un objeto sea observable por SwiftUI, debe conformar al protocolo ObservableObject. Las propiedades dentro de este objeto que queremos que SwiftUI observe deben marcarse con @Published.

class UsuarioViewModel: ObservableObject {
    @Published var nombre: String = "" {
        didSet { print("Nombre cambiado a: \(nombre)") }
    }
    @Published var edad: Int = 0

    func actualizarNombre(nuevoNombre: String) {
        nombre = nuevoNombre
    }
}

@ObservedObject: Observando un Objeto Existente

@ObservedObject se utiliza para observar una instancia de ObservableObject que se crea fuera de la vista o se pasa a ella. SwiftUI no gestiona el ciclo de vida de este objeto; si la vista se recrea (por ejemplo, en una lista), el objeto se recrea o se pierde a menos que se maneje cuidadosamente.

struct PerfilView: View {
    @ObservedObject var viewModel: UsuarioViewModel

    var body: some View {
        VStack {
            Text("Nombre: \(viewModel.nombre)")
            TextField("Introduce nombre", text: $viewModel.nombre)
            Button("Actualizar Edad") {
                viewModel.edad += 1
            }
        }
    }
}
⚠️ Advertencia: Si la vista `PerfilView` se destruye y se recrea, se perderá el estado de `viewModel` a menos que este se haya creado y pasado desde un punto superior de la jerarquía de vistas o se haya mantenido vivo de otra forma. Esto lo resuelve `@StateObject`.

@StateObject: Creando y Poseyendo un Objeto Observable

Introducido en iOS 14, @StateObject es la forma correcta de crear y poseer una instancia de ObservableObject dentro de una vista. SwiftUI gestiona el ciclo de vida de este objeto, asegurando que se inicialice una vez y persista mientras la vista que lo posee esté en la jerarquía, incluso si la vista se redibuja o recrea.

struct PrincipalView: View {
    @StateObject private var usuarioVM = UsuarioViewModel()

    var body: some View {
        VStack {
            Text("Hola, \(usuarioVM.nombre)!")
            TextField("Tu nombre", text: $usuarioVM.nombre)
            NavigationLink("Ir a Detalles", destination: PerfilView(viewModel: usuarioVM))
        }
    }
}
🔥 Importante: Usa `@StateObject` cuando la vista *posee* el objeto observable y es responsable de su ciclo de vida. Usa `@ObservedObject` cuando la vista *recibe* un objeto observable que ya existe.

🌍 Compartiendo Estado en la Jerarquía de Vistas: @EnvironmentObject

Cuando necesitas compartir un ObservableObject a través de múltiples niveles de la jerarquía de vistas sin pasarlo manualmente de padre a hijo, @EnvironmentObject es la solución ideal. Es una forma de inyección de dependencias para SwiftUI.

Inyectando un EnvironmentObject

Para usar @EnvironmentObject, primero debes inyectar una instancia de tu ObservableObject en el entorno de alguna vista padre (o en la raíz de tu aplicación) usando el modificador .environmentObject().

@main
struct MyApp: App {
    @StateObject var appSettings = AppSettings() // ObservableObject para ajustes globales

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appSettings) // Inyectamos en la raíz
        }
    }
}

class AppSettings: ObservableObject {
    @Published var temaOscuro: Bool = false
    @Published var tamanoFuente: Double = 16.0
}

Consumiendo un EnvironmentObject

Cualquier vista descendiente puede acceder a este objeto usando @EnvironmentObject sin necesidad de pasarlo como parámetro.

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Bienvenido!")
                NavigationLink("Ajustes", destination: AjustesView())
            }
        }
    }
}

struct AjustesView: View {
    @EnvironmentObject var appSettings: AppSettings

    var body: some View {
        Form {
            Toggle("Tema Oscuro", isOn: $appSettings.temaOscuro)
            Stepper("Tamaño Fuente: \(appSettings.tamanoFuente, specifier: "%.0f")", value: $appSettings.tamanoFuente, in: 10...24)
        }
        .navigationTitle("Configuración")
    }
}
Main App @main AppSettings EnvironmentObject .environmentObject() ContentView (Vista Principal) AjustesView @EnvironmentObject access Acceso directo

🔄 Integrando Combine para Flujos de Datos Reactivos

Combine es el framework de Apple para manejar eventos asíncronos en Swift. Se integra perfectamente con SwiftUI para crear flujos de datos reactivos más sofisticados.

Publisher y Subscriber Básicos

Combine se basa en tres componentes principales:

  • Publishers: Producen valores a lo largo del tiempo.
  • Operators: Transforman los valores emitidos por los publishers.
  • Subscribers: Reciben y reaccionan a los valores emitidos por los publishers.

Los @Published de ObservableObject son en realidad Publishers de Combine.

import Combine

class DataManager: ObservableObject {
    @Published var data: [String] = []
    var cancellables = Set<AnyCancellable>()

    init() {
        // Un ejemplo de cómo un publisher podría actualizar nuestro estado
        Timer.publish(every: 2, on: .main, in: .common)
            .autoconnect()
            .map { _ in "Dato generado a las \(Date().formatted())" }
            .sink { [weak self] newData in
                self?.data.append(newData)
            }
            .store(in: &cancellables)
    }
}

@StateObject con Combine para Datos Asíncronos

Es común usar @StateObject para poseer un ViewModel o Service que utiliza Combine para realizar operaciones asíncronas, como llamadas a red.

class APIService: ObservableObject {
    @Published var posts: [String] = []
    var cancellables = Set<AnyCancellable>()

    func fetchPosts() {
        URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/posts")!)
            .map { $0.data }
            .decode(type: [Post].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                if case .failure(let error) = completion {
                    print("Error fetching posts: \(error.localizedDescription)")
                }
            }, receiveValue: { [weak self] fetchedPosts in
                self?.posts = fetchedPosts.map { $0.title }
            })
            .store(in: &cancellables)
    }

    struct Post: Codable, Identifiable {
        let id: Int
        let title: String
        let body: String
    }
}

struct PostsView: View {
    @StateObject private var apiService = APIService()

    var body: some View {
        NavigationView {
            List(apiService.posts, id: \.self) {
                Text($0)
            }
            .navigationTitle("Publicaciones")
            .onAppear {
                apiService.fetchPosts()
            }
        }
    }
}

🎯 Patrones Avanzados de Gestión de Estado

Para aplicaciones más grandes y complejas, es útil adoptar patrones que estructuran el código y hacen que la gestión del estado sea más predecible y escalable.

MVVM (Model-View-ViewModel)

MVVM es un patrón popular en SwiftUI. Separa la lógica de negocio y presentación de la UI. El ViewModel actúa como un intermediario entre el Model (datos) y la View (UI), exponiendo datos y comandos de forma que la vista pueda consumirlos fácilmente. Los ViewModel suelen ser ObservableObject.

Componentes:

  • Model: Representa los datos y la lógica de negocio.
  • View: La interfaz de usuario. Observa el ViewModel.
  • ViewModel: Contiene la lógica de presentación, transforma los datos del Model para la View y maneja las interacciones del usuario. Es un ObservableObject.
MODEL Datos y Lógica VIEWMODEL Estado y Lógica UI VIEW Interfaz Usuario Acciones Solicitar Datos Notifica cambios @Published SE REDIBUJA Arquitectura MVVM
// Model (simplificado)
struct Tarea: Identifiable, Codable {
    let id = UUID()
    var titulo: String
    var completada: Bool
}

// ViewModel
class TareasViewModel: ObservableObject {
    @Published var tareas: [Tarea] = []

    init() {
        // Cargar tareas al inicio (ejemplo)
        tareas = [Tarea(titulo: "Comprar leche", completada: false),
                  Tarea(titulo: "Estudiar SwiftUI", completada: true)]
    }

    func agregarTarea(titulo: String) {
        tareas.append(Tarea(titulo: titulo, completada: false))
    }

    func toggleCompletada(tarea: Tarea) {
        if let index = tareas.firstIndex(where: { $0.id == tarea.id }) {
            tareas[index].completada.toggle()
        }
    }

    func eliminarTarea(at offsets: IndexSet) {
        tareas.remove(atOffsets: offsets)
    }
}

// View
struct TareasListView: View {
    @StateObject private var vm = TareasViewModel()
    @State private var nuevaTareaTitulo: String = ""

    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    TextField("Nueva tarea", text: $nuevaTareaTitulo)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                    Button("Agregar") {
                        vm.agregarTarea(titulo: nuevaTareaTitulo)
                        nuevaTareaTitulo = ""
                    }
                }
                .padding()

                List {
                    ForEach(vm.tareas) { tarea in
                        HStack {
                            Image(systemName: tarea.completada ? "checkmark.circle.fill" : "circle")
                                .onTapGesture { vm.toggleCompletada(tarea: tarea) }
                            Text(tarea.titulo)
                                .strikethrough(tarea.completada)
                        }
                    }
                    .onDelete(perform: vm.eliminarTarea)
                }
                .navigationTitle("Mis Tareas")
            }
        }
    }
}

Binding para Flujos de Datos Complejos

Más allá de pasar el estado simple de una vista padre a una hija, podemos crear Bindings personalizados para conectar directamente propiedades o sub-estados complejos.

Ejemplo de Binding personalizado

Imagina que tienes una lista de usuarios y quieres editar uno en una vista de detalle. En lugar de pasar todo el array y tener que encontrar y actualizar el usuario específico, puedes pasar un Binding al usuario concreto.

struct UsuarioEditorView: View {
    @Binding var usuario: Usuario

    var body: some View {
        Form {
            TextField("Nombre", text: $usuario.nombre)
            TextField("Email", text: $usuario.email)
        }
        .navigationTitle("Editar Usuario")
    }
}

struct ListaUsuariosView: View {
    @State private var usuarios: [Usuario] = [
        Usuario(nombre: "Alice", email: "alice@example.com"),
        Usuario(nombre: "Bob", email: "bob@example.com")
    ]

    var body: some View {
        NavigationView {
            List {
                ForEach($usuarios) { $usuario in // Iteramos sobre Binding<Usuario>
                    NavigationLink(destination: UsuarioEditorView(usuario: $usuario)) {
                        Text(usuario.nombre)
                    }
                }
            }
            .navigationTitle("Usuarios")
        }
    }
}

struct Usuario: Identifiable {
    let id = UUID()
    var nombre: String
    var email: String
}

UserDefaults con @AppStorage y @SceneStorage

SwiftUI también ofrece Property Wrappers para persistencia de datos básicos.

  • @AppStorage (iOS 14+): Almacena y recupera valores de UserDefaults. Ideal para preferencias de usuario simples y persistentes.
struct AjustesPreferenciasView: View {
    @AppStorage("temaClaroActivado") var temaClaro: Bool = false

    var body: some View {
        Toggle("Activar Tema Claro", isOn: $temaClaro)
    }
}
  • @SceneStorage (iOS 14+): Persiste estado por escena (window) de la aplicación, útil para restaurar el estado de la UI cuando la aplicación se cierra y se reabre o cuando una escena se restaura. Los datos solo persisten durante el ciclo de vida de la escena y no son accesibles entre diferentes escenas o instancias de la aplicación.
struct DocumentEditorView: View {
    @SceneStorage("documentoActual") var documento: String?

    var body: some View {
        TextEditor(text: Binding(get: { documento ?? "" }, set: { documento = $0 }))
            .frame(minWidth: 200, minHeight: 150)
            .border(Color.gray)
            .padding()
            .navigationTitle("Editor")
    }
}

📈 Optimizando el Rendimiento y la Depuración

Una gestión de estado eficaz no solo hace que tu código sea más limpio, sino también más performante.

Redibujados Innecesarios

SwiftUI es eficiente, pero un mal uso de las Property Wrappers puede llevar a redibujados excesivos de las vistas. Usa @State para el estado que es verdaderamente local y no necesita persistir o ser compartido ampliamente. Asegúrate de que tus ObservableObjects solo emitan cambios (@Published) cuando sea realmente necesario.

💡 Consejo: Utiliza `print()` o un `breakpoint` dentro del `body` de tu vista para ver cuándo se está redibujando.

Dividir Vistas para Mejorar el Rendimiento

Grandes vistas monolíticas con mucho estado pueden causar redibujados ineficientes. Divide tu UI en componentes más pequeños y reutilizables. Cada vista solo se redibujará cuando su propio estado o sus Bindings/EnvironmentObjects cambien.

Uso de Equateable para Vistas Estáticas

Para vistas que no necesitan reaccionar a cambios de estado o que su contenido no varía a menos que sus propiedades cambien explícitamente, puedes hacer que conformen a Equatable y usar ContentView.equatable(). SwiftUI solo redibujará la vista si sus propiedades de entrada cambian.

struct StaticTextView: View, Equatable {
    let message: String

    var body: some View {
        Text(message)
    }

    static func == (lhs: StaticTextView, rhs: StaticTextView) -> Bool {
        lhs.message == rhs.message
    }
}

✅ Resumen y Mejores Prácticas

Elegir la Property Wrapper adecuada para cada situación es clave. Aquí tienes una tabla resumen:

Property WrapperUso PrincipalCiclo de VidaPersistenciaAlcance
@StateEstado simple, local a una vistaGestionado por la vistaNoLocal a la vista
@BindingEnlace bidireccional a @State o @StateObjectNo gestiona ciclo de vida, referencia externaNoCompartido con padre
@ObservedObjectObservar objeto ObservableObject existenteNo gestiona ciclo de vida, se espera externoNoRecibido por la vista
@StateObjectCrear y poseer ObservableObject dentro de una vistaGestionado por la vista, persiste durante vida de la vistaNoPoseído por la vista
@EnvironmentObjectCompartir ObservableObject en toda la jerarquíaGestionado por el inyector (app/padre)NoGlobal/Jerárquico
@AppStoragePersistir valores en UserDefaultsAutomático con UserDefaultsGlobal de la app
@SceneStoragePersistir valores por escenaAutomático por escena, no globalLocal de la escena

Consideraciones Finales

  • Empieza simple: Para estado local, @State es tu mejor amigo.
  • Aísla la lógica: Usa ObservableObject con @StateObject para encapsular la lógica de negocio y presentación.
  • Inyección de Dependencias: @EnvironmentObject es excelente para dependencias compartidas a nivel de aplicación o subsistema.
  • Combine: Potencia tus ObservableObjects con Combine para manejar asincronía y reactividad compleja.
  • Pruebas: Diseña tus ViewModels para que sean fácilmente testeables de forma independiente de la UI.

Dominar estos conceptos te permitirá construir aplicaciones SwiftUI robustas, escalables y fáciles de mantener. La clave está en comprender el flujo de datos y el ciclo de vida de cada tipo de estado.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!