Programación en Swift y SwiftUI para iOS Developers

@Observable en SwiftUI para iOS, macOS y watchOS

El desarrollo en el ecosistema de Apple está en constante evolución. Si llevas tiempo trabajando con SwiftUI, seguramente has lidiado con la “sopa de letras” de la gestión de estado: @StateObject@ObservedObject@EnvironmentObjecty el siempre presente @Published. Aunque funcionales, estos property wrappers venían con una carga cognitiva y de rendimiento que a menudo complicaba la arquitectura de nuestras apps.

Con la llegada de Swift 5.9 (iOS 17, macOS 14, watchOS 10), Apple introdujo un cambio de paradigma masivo: el marco de trabajo Observation y la macro @Observable.

Este artículo es una guía técnica diseñada para llevarte de cero a experto en el uso de @Observable, explicándote no solo cómo usarlo, sino por qué cambia radicalmente la forma en que desarrollamos para iOS, macOS y watchOS.


Parte 1: ¿Qué es @Observable y por qué lo necesitamos?

Para entender el futuro, primero debemos mirar brevemente al pasado. Antes de Swift 5.9, SwiftUI dependía fuertemente del framework Combine para la gestión de datos reactivos.

El problema del “Viejo Mundo” (ObservableObject)

Cuando usábamos ObservableObject, teníamos que marcar explícitamente cada propiedad que queríamos que la vista “vigilara” con @Published.

El problema fundamental era la granularidad. Si tenías un objeto con 10 propiedades marcadas como @Published, y una vista solo usaba una de ellas, la vista se invalidaba y redibujaba incluso si cambiaba una de las otras 9 propiedades que la vista ni siquiera estaba mostrando. Esto generaba redibujados innecesarios y cuellos de botella en el rendimiento en aplicaciones complejas.

La Solución: La Macro @Observable

@Observable no es un property wrapper tradicional; es una Macro. Las macros en Swift transforman tu código en tiempo de compilación.

Al añadir @Observable antes de una clase, la macro reescribe automáticamente esa clase para conformar al protocolo Observable. Lo revolucionario aquí es el seguimiento preciso de dependencias.

Ventajas Clave:

  1. Sintaxis Limpia: Adiós a @Published. Las propiedades son observadas por defecto.
  2. Rendimiento Superior: Las vistas solo se actualizan si cambia una propiedad que la vista leyó específicamente en su cuerpo (body).
  3. Menos Property Wrappers: Simplifica drásticamente la inyección de dependencias.

Parte 2: Configurando el Entorno en Xcode

Antes de escribir código, asegúrate de tener las herramientas correctas. @Observable requiere:

  • Xcode: Versión 15.0 o superior.
  • Target: iOS 17+, macOS 14+, watchOS 10+, tvOS 17+ o visionOS.

Si estás manteniendo una app que debe soportar iOS 16 o inferior, no podrás migrar completamente a este sistema todavía (aunque puedes usar código condicional).


Parte 3: Tutorial Práctico – Creando un Modelo de Datos

Vamos a construir un sistema de gestión de un “Perfil de Usuario” que funcione a través de iPhone, Mac y Apple Watch.

Paso 1: Definiendo el Modelo

En el modelo antiguo, haríamos esto:

// MÉTODO ANTIGUO (Deprecado conceptualmente)
class UserProfileOld: ObservableObject {
    @Published var name: String = "Dev"
    @Published var isPremium: Bool = false
    var lastLogin: Date = Date() // No notifica cambios
}

Con @Observable, el código se ve así:

import Observation
import Foundation

@Observable
class UserProfile {
    var name: String = "Dev"
    var isPremium: Bool = false
    var lastLogin: Date = Date()
    
    // Propiedades calculadas también son observadas automáticamente
    var greeting: String {
        return "Hola, \(name)"
    }
    
    // Podemos excluir propiedades de la observación si es necesario
    @ObservationIgnored var analyticsId: String = "XYZ-123"
    
    init(name: String, isPremium: Bool) {
        self.name = name
        self.isPremium = isPremium
    }
}

Nota Técnica: Al compilar, la macro @Observable inyecta código que implementa el método access(keyPath:) y withMutation(keyPath:). Esto conecta las propiedades con el grafo de dependencias de SwiftUI de forma invisible para ti.


Parte 4: Inyección y Uso en las Vistas (iOS/macOS/watchOS)

Aquí es donde la magia ocurre. La forma en que inyectamos los datos cambia sutilmente pero con gran impacto.

1. La “Fuente de la Verdad” (@State)

Antes, @State era solo para tipos de valor (structs, ints, bools). Para clases usábamos @StateObject. Ahora, @State puede gestionar el ciclo de vida de objetos @Observable.

import SwiftUI

struct ContentView: View {
    // Ya no necesitamos @StateObject. @State es suficiente.
    @State private var user = UserProfile(name: "Ana", isPremium: true)
    
    var body: some View {
        VStack {
            // SwiftUI detecta que leemos 'user.name'.
            // Si 'user.isPremium' cambia, esta vista NO se redibujará. ¡Magia!
            Text(user.greeting)
                .font(.largeTitle)
            
            EditProfileView(user: user)
        }
        .padding()
    }
}

2. Pasando datos a hijos (Binding vs Let)

Si solo necesitas leer el objeto en una vista hija, pásalo como una propiedad normal (let o var). No necesitas @ObservedObject.

struct ReadOnlyView: View {
    // Simplemente declaramos el tipo. Sin wrappers.
    let user: UserProfile 
    
    var body: some View {
        Text("Estado: \(user.isPremium ? "Premium" : "Básico")")
    }
}

3. Creando Bindings (@Bindable)

Este es el punto que más confunde a los desarrolladores al principio. Si necesitamos crear un binding (usar el símbolo $) para controles como TextFieldToggle o Stepper, no podemos usar el objeto tal cual. Necesitamos el wrapper @Bindable.

@Bindable crea enlaces ligeros a las propiedades del objeto observable.

struct EditProfileView: View {
    // Usamos @Bindable para poder acceder a los $bindings
    @Bindable var user: UserProfile
    
    var body: some View {
        Form {
            Section("Editar Información") {
                // Aquí necesitamos el $, por eso usamos @Bindable arriba
                TextField("Nombre", text: $user.name)
                
                Toggle("Suscripción Premium", isOn: $user.isPremium)
            }
        }
    }
}

Si intentaras usar var user: UserProfile sin @Bindable, Xcode te daría un error al intentar acceder a $user.name.


Parte 5: Gestión del Entorno (Environment)

El patrón de inyección de dependencias también se ha simplificado. EnvironmentObject (que dependía de tipos dinámicos y causaba crashes si olvidabas inyectarlo) se sustituye por el uso más seguro de .environment.

Inyectando en la raíz (App)

@main
struct MyApp: App {
    @State private var user = UserProfile(name: "Carlos", isPremium: false)

    var body: some Scene {
        WindowGroup {
            ContentView()
                // Inyectamos el objeto en el entorno
                .environment(user)
        }
    }
}

Leyendo en la vista

Para leerlo, usamos el wrapper @Environment, no EnvironmentObject.

struct SettingsView: View {
    // Recuperamos el objeto basado en su TIPO
    @Environment(UserProfile.self) var user
    
    var body: some View {
        if user.isPremium {
            Text("Ajustes Avanzados")
        } else {
            Text("Actualiza a Premium para ver más")
        }
    }
}

Este cambio unifica la forma en que pasamos valores simples (como ColorScheme) y nuestros objetos de datos complejos.


Parte 6: Adaptabilidad Multiplataforma

Una de las grandes promesas de SwiftUI es “Learn once, apply anywhere”. El código de datos que hemos escrito arriba es 100% idéntico para iOS, macOS y watchOS. La diferencia radica en cómo renderizamos la UI.

Ejemplo para watchOS

En un Apple Watch, el espacio es limitado. Podríamos tener una vista específica:

// WatchOS Specific View
struct WatchUserProfileView: View {
    @State private var user = UserProfile(name: "Ana", isPremium: true)
    
    var body: some View {
        NavigationStack {
            List {
                // El Toggle se adapta visualmente al estilo de watchOS
                Toggle(isOn: $user.isPremium) {
                    Label("Premium", systemImage: "star.fill")
                }
            }
            .navigationTitle(user.name)
        }
    }
}

Ejemplo para macOS

En macOS, podríamos querer una ventana de ajustes separada:

// macOS Settings View
struct MacSettingsView: View {
    @Environment(UserProfile.self) var user
    
    var body: some View {
        TabView {
            Form {
                TextField("Nombre", text: Bindable(user).name)
                // Nota: A veces podemos usar Bindable() inline si no queremos el wrapper global
            }
            .tabItem { Label("General", systemImage: "gear") }
        }
        .scenePadding()
    }
}

Truco Pro: Nota el uso de Bindable(user).name en el ejemplo de macOS. Si no quieres decorar la propiedad de la vista con @Bindable, puedes crear un bindable “al vuelo” dentro del body.


Parte 7: Casos Avanzados y Migración

Arrays y Colecciones

Uno de los puntos débiles de ObservableObject era observar cambios dentro de arrays de objetos. Con @Observable, la gestión es más fluida, pero requiere atención.

Si tienes un array de modelos @Observablevar users: [UserProfile]

SwiftUI detectará:

  1. Si añades o eliminas elementos del array.
  2. Si cambias una propiedad de uno de los elementos (siempre que la vista esté pintando ese elemento específico).

Migración Progresiva

No necesitas reescribir toda tu app en un día. Puedes mezclar ObservableObject y @Observable.

  • Las vistas nuevas pueden usar el nuevo sistema.
  • Las vistas antiguas pueden seguir usando @StateObject.

Sin embargo, no mezcles ambos en la misma clase. Una clase no debe ser @Observable y ObservableObject al mismo tiempo.

Errores Comunes (Gotchas)

  1. Structs vs Clases: @Observable está diseñado para clases (tipos de referencia). Si usas structs, sigue usando @Statesimple.
  2. Ciclos de Retención: Al igual que antes, ten cuidado con los closures dentro de tus clases observables. Usa [weak self].
  3. Vistas que no actualizan: Si una vista no se actualiza, verifica que realmente estás leyendo la propiedad en el body. Si solo lees la propiedad en una función que se llama (pero cuyo resultado no cambia la estructura de la vista), es posible que el sistema de observación no registre la dependencia.

Tabla Resumen: El Antes y El Después

ConceptoAntes (SwiftUI Legacy)Ahora (SwiftUI Moderno)
Definiciónclass Model: ObservableObject@Observable class Model
Publicación@Published var datavar data (Automático)
Creación (Dueño)@StateObject var model = Model()@State var model = Model()
Consumo (Lectura)@ObservedObject var modellet model: Model
Consumo (Binding)@ObservedObject / @Binding@Bindable var model
Entorno@EnvironmentObject var model@Environment(Model.self) var model

Conclusión

La introducción de @Observable en Swift 5.9 es más que azúcar sintáctico; es una reconstrucción fundamental de cómo fluyen los datos en nuestras aplicaciones. Al eliminar la dependencia explícita de Combine para la capa de vista y aprovechar el poder de las Macros, Apple ha entregado una herramienta que es a la vez más fácil de enseñar a los principiantes y mucho más potente para los expertos preocupados por el rendimiento.

Para desarrolladores trabajando en el ecosistema Apple, adoptar @Observable no es opcional a largo plazo. Es el estándar que definirá el desarrollo en SwiftUI durante los próximos años. Reduce el código repetitivo, elimina errores comunes de invalidación de vistas y hace que nuestras aplicaciones se sientan más rápidas y fluidas.

Ya sea que estés construyendo la próxima gran app para iOS, una utilidad compleja para macOS o una experiencia ligera para watchOS, @Observable es tu nuevo mejor amigo.

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

Mejores frameworks para SwiftUI

Next Article

@Observable vs @Published en SwiftUI

Related Posts