@Observable y Observation Framework en SwiftUI: Migración Completa desde ObservableObject para iOS 26

Aprende a migrar de ObservableObject a @Observable en SwiftUI iOS 26: paso a paso, ejemplos de código, @Bindable, @ObservationIgnored y mejor rendimiento.

@Observable SwiftUI: Guía iOS 26

Actualizado: 8 de junio de 2026

El macro @Observable es el reemplazo oficial de ObservableObject en SwiftUI desde iOS 17, y en iOS 26 sigue siendo la API recomendada para gestionar estado en tipos de referencia: marcas tu clase con @Observable, eliminas @Published de cada propiedad, y SwiftUI rastrea automáticamente qué propiedades lee cada vista, redibujándola solo cuando cambian las propiedades que realmente usa. Esa granularidad, imposible con ObservableObject, se traduce en menos invalidaciones, menos jerarquías re-evaluadas y un rendimiento bastante mejor en pantallas complejas.

  • @Observable es un macro del framework Observation disponible desde iOS 17, iPadOS 17, macOS 14, tvOS 17 y watchOS 10; en iOS 26 ya es la API por defecto en todas las plantillas de Xcode.
  • Sustituye al protocolo ObservableObject y al property wrapper @Published: marcas la clase con @Observable y todas las propiedades almacenadas se rastrean automáticamente.
  • En las vistas dejas de usar @StateObject y @ObservedObject; usas @State para crear la instancia y @Bindable cuando necesitas bindings de doble vía con tipos de referencia.
  • El rastreo es por propiedad, no por objeto: una vista solo se invalida si lee una propiedad que cambió, lo que reduce redibujados innecesarios y mejora el rendimiento en listas grandes.
  • Para excluir propiedades del rastreo (caches, identificadores internos, observadores manuales) usa @ObservationIgnored.
  • La migración desde ObservableObject se puede hacer incremental clase por clase, y convive sin problemas con código heredado durante la transición.

¿Qué es el framework Observation y por qué reemplaza a ObservableObject?

El framework Observation es una API del estándar de Swift que implementa el patrón observador mediante macros. Se presentó en la WWDC23 y está disponible en producción desde iOS 17. En iOS 26 ya se ha consolidado como la única API recomendada por Apple para exponer estado mutable a SwiftUI: las plantillas de Xcode 26 generan código con @Observable por defecto, y la documentación de SwiftData, Charts, MapKit y las nuevas APIs de Apple Intelligence asumen su uso.

La razón técnica del cambio es el modelo de invalidación. Con ObservableObject, cualquier modificación a una propiedad marcada con @Published publica un evento objectWillChange que invalida toda vista que observe la instancia, sin importar si esa vista leyó realmente la propiedad modificada.

Honestamente, este es el cuello de botella que más he visto en apps reales. En una pantalla con cincuenta filas que comparten un view model, cambiar el contador de una sola fila redibuja las cincuenta. El framework Observation, por el contrario, rastrea accesos a nivel de propiedad mediante código generado por el macro: SwiftUI sabe exactamente qué propiedades leyó el body de cada vista y solo la reevalúa cuando una de esas propiedades muta.

Hay un beneficio adicional menos obvio: las propiedades opcionales y las colecciones de objetos (que con ObservableObject requerían acrobacias con @Published y publicadores Combine) se observan de forma transparente. Esto simplifica modelos de datos jerárquicos como árboles de navegación, grafos de entidades o estructuras anidadas que antes obligaban a propagar manualmente objectWillChange.

¿Cuál es la diferencia entre @Observable y @StateObject?

@Observable es un macro que se aplica a la declaración de la clase para hacerla observable, mientras que @StateObject es un property wrapper que se aplica a una propiedad de la vista para indicar que ésta posee y mantiene viva una instancia de un ObservableObject. No son intercambiables: pertenecen a niveles distintos del modelo.

Con el framework Observation desaparece la necesidad de @StateObject, @ObservedObject y @EnvironmentObject. Las vistas usan los mismos wrappers que ya conoces para tipos valor: @State para crear y poseer la instancia, @Environment para leer instancias inyectadas en el entorno y @Bindable cuando necesitas pasar bindings de doble vía a controles como TextField o Toggle. La tabla siguiente resume el mapeo:

Caso de usoObservableObject (legado)@Observable (moderno)
Marcar la clase como observable: ObservableObject + @Published por propiedad@Observable sobre la clase
La vista crea y posee la instancia@StateObject@State
La vista recibe una instancia externa@ObservedObjectpropiedad normal (let / var)
Inyectar en el entorno@EnvironmentObject + .environmentObject(_:)@Environment + .environment(_:)
Pasar binding a control$viewModel.property (directo)@Bindable + $model.property
Excluir propiedad del rastreoomitir @Published@ObservationIgnored
Granularidad de invalidaciónobjeto completopor propiedad leída
Compatibilidad mínimaiOS 13+iOS 17+ / macOS 14+

La consecuencia práctica es que el código se simplifica. Hay menos decisiones que tomar sobre qué wrapper usar y la lógica de propiedad de la instancia se vuelve idéntica a la de un tipo valor. Si te interesa profundizar en patrones modernos de SwiftUI, te recomendamos la guía de NavigationStack en SwiftUI, que aplica @Observable al modelo de navegación.

Cómo migrar de ObservableObject a @Observable paso a paso

La migración es bastante mecánica, y lo mejor es que se puede hacer clase por clase sin tocar el resto de la app. Estos son los cuatro pasos canónicos según la documentación oficial de Apple sobre migración a Observation:

  1. Importa Observation en el archivo (Swift lo añade automáticamente si usas Xcode 16+).
  2. Elimina la conformidad : ObservableObject de la clase y aplica el macro @Observable.
  3. Elimina todas las anotaciones @Published de las propiedades almacenadas.
  4. En las vistas, sustituye @StateObject por @State, elimina @ObservedObject de propiedades pasadas como dependencia, y reemplaza @EnvironmentObject por @Environment.

Ejemplo: clase y vista antes y después

Vamos a migrar un view model de un contador con preferencias de usuario. La versión heredada con ObservableObject:

// Antes: API basada en ObservableObject (iOS 13+)
import SwiftUI
import Combine

final class CounterStore: ObservableObject {
    @Published var count: Int = 0
    @Published var step: Int = 1
    @Published var username: String = ""

    func increment() {
        count += step
    }
}

struct CounterView: View {
    @StateObject private var store = CounterStore()

    var body: some View {
        VStack {
            Text("Total: \(store.count)")
            TextField("Usuario", text: $store.username)
            Stepper("Paso: \(store.step)", value: $store.step, in: 1...10)
            Button("Incrementar", action: store.increment)
        }
        .padding()
    }
}

La misma funcionalidad con el framework Observation:

// Después: API basada en @Observable (iOS 17+)
import SwiftUI
import Observation

@Observable
final class CounterStore {
    var count: Int = 0
    var step: Int = 1
    var username: String = ""

    func increment() {
        count += step
    }
}

struct CounterView: View {
    @State private var store = CounterStore()

    var body: some View {
        @Bindable var store = store // expone bindings a controles

        VStack {
            Text("Total: \(store.count)")
            TextField("Usuario", text: $store.username)
            Stepper("Paso: \(store.step)", value: $store.step, in: 1...10)
            Button("Incrementar", action: store.increment)
        }
        .padding()
    }
}

Fíjate en tres detalles: la clase ya no conforma ObservableObject, no hay @Published, y la vista usa @State en lugar de @StateObject. La línea @Bindable var store = store dentro del body es opcional. Solo es necesaria si vas a generar bindings con el prefijo $ hacia controles.

Cuándo usar @Bindable en SwiftUI

Usa @Bindable cuando necesites pasar un Binding<T> hacia un control (TextField, Toggle, Slider, Picker) desde un tipo de referencia marcado con @Observable. Es el equivalente moderno a la sintaxis $viewModel.property que ofrecía @ObservedObject.

El wrapper se puede aplicar en tres lugares:

  • Como propiedad de la vista: @Bindable var model: User cuando la instancia se inyecta como dependencia.
  • Como variable local dentro del body: @Bindable var model = model cuando la instancia vino por @State o @Environment y solo necesitas bindings puntualmente.
  • En parámetros de funciones que construyen subvistas y necesitan generar bindings.
@Observable
final class UserSettings {
    var notificationsEnabled: Bool = true
    var displayName: String = ""
}

struct SettingsForm: View {
    @Bindable var settings: UserSettings // recibida desde el padre

    var body: some View {
        Form {
            TextField("Nombre", text: $settings.displayName)
            Toggle("Notificaciones", isOn: $settings.notificationsEnabled)
        }
    }
}

struct ParentView: View {
    @State private var settings = UserSettings()

    var body: some View {
        SettingsForm(settings: settings) // se pasa la instancia, no un binding
    }
}

Nota cómo el padre pasa la instancia directamente (settings: settings), no un binding. @Bindable en el hijo se encarga de generar los bindings cuando se invocan con $. Este patrón elimina la propagación manual de bindings a través de la jerarquía y reduce bastante el ruido sintáctico.

Excluir propiedades del rastreo con @ObservationIgnored

Por defecto, el macro @Observable rastrea todas las propiedades almacenadas de la clase. Eso suele ser lo que quieres, pero hay casos donde rastrear una propiedad es innecesario o incluso problemático:

  • Caches internas que se invalidan con lógica propia.
  • Identificadores constantes (UUID, claves de base de datos) que no cambian.
  • Subscripciones a Combine, observadores de Notification o tareas en curso.
  • Dependencias inyectadas que no representan estado mutable de UI.

Para esos casos aplica @ObservationIgnored a la propiedad. SwiftUI no la incluirá en el rastreo y las vistas no se invalidarán al modificarla:

@Observable
final class FeedStore {
    var posts: [Post] = []      // rastreada -> invalida vistas
    var isLoading: Bool = false // rastreada -> invalida vistas

    @ObservationIgnored
    private var cancellables: Set<AnyCancellable> = []

    @ObservationIgnored
    private let apiClient: APIClient

    init(apiClient: APIClient) {
        self.apiClient = apiClient
    }
}

Por qué @Observable mejora el rendimiento

La mejora de rendimiento de @Observable proviene de dos cambios fundamentales en cómo SwiftUI decide qué redibujar. Primero, el rastreo es por propiedad accedida durante la evaluación del body, no por instancia. Segundo, el código generado por el macro se integra con el motor de difusión de SwiftUI mediante la función withObservationTracking, que devuelve exactamente el conjunto de KeyPaths leídos.

El impacto se nota especialmente en tres escenarios:

  1. Listas y grids con view models compartidos: antes, mutar una propiedad obligaba a reevaluar todas las celdas. Ahora, solo las celdas que leen la propiedad modificada se invalidan.
  2. Modelos con muchas propiedades: una pantalla de formulario con veinte campos antes redibujaba el formulario entero al teclear; ahora cada TextField es independiente.
  3. Jerarquías profundas con @EnvironmentObject: el viejo modelo invalidaba todas las vistas que leían el entorno, aunque ignorasen el campo cambiado. @Environment con un tipo @Observable solo invalida las que leen el campo afectado.

Mediciones publicadas por la comunidad (incluyendo el análisis de SwiftLee sobre el rendimiento de @Observable) muestran reducciones significativas en evaluaciones de body en pantallas tipo "feed" con cientos de elementos. Para apps con interfaces de datos densos (dashboards financieros, editores, herramientas de productividad), la migración suele traducirse en menos saltos de framerate y menor consumo de CPU.

Patrones avanzados: inyección, entornos y subvistas

Cuando una app crece, surge la necesidad de compartir instancias @Observable entre múltiples vistas. SwiftUI ofrece tres mecanismos limpios para hacerlo sin recurrir a singletons.

Inyección por entorno con @Environment

Para tipos @Observable, @EnvironmentObject ya no es necesario. Usa @Environment con el tipo directamente y exponlo con el modificador .environment(_:):

@Observable
final class AuthSession {
    var currentUser: User?
}

@main
struct MyApp: App {
    @State private var session = AuthSession()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(session)
        }
    }
}

struct ProfileView: View {
    @Environment(AuthSession.self) private var session

    var body: some View {
        if let user = session.currentUser {
            Text("Hola, \(user.name)")
        } else {
            Text("Invitado")
        }
    }
}

Composición con SwiftData y modelos persistidos

Los modelos de SwiftData ya son observables por construcción: el macro @Model incluye la maquinaria de @Observable. Esto significa que tu capa de persistencia se integra de forma transparente con las vistas sin código adicional. Si trabajas con SwiftData, consulta nuestra guía completa de SwiftData en iOS 26 para ver cómo combinar @Model con view models @Observable que orquestan lógica de aplicación.

Concurrencia y aislamiento

Las clases @Observable no son Sendable automáticamente. En Swift 6, si compartes una instancia entre tareas, deberás declararla aislada a un actor (típicamente @MainActor para view models que tocan UI) o conformarla a Sendable con sincronización manual. Para profundizar en este tema te recomendamos el artículo sobre concurrencia en Swift 6.2 con async/await y actores.

@MainActor
@Observable
final class DashboardViewModel {
    var widgets: [Widget] = []
    var isRefreshing: Bool = false

    func refresh() async {
        isRefreshing = true
        defer { isRefreshing = false }
        widgets = await loadWidgets()
    }
}

Errores comunes al adoptar @Observable

La transición es sencilla, pero hay tropiezos recurrentes que conviene anticipar. Yo mismo he caído en casi todos ellos:

  • Usar var model = Model() en lugar de @State private var model = Model(): sin @State, la instancia se recrea en cada evaluación del body y pierdes todo el estado. Es el error más frecuente al migrar desde @StateObject.
  • Olvidar @Bindable al pasar bindings: si intentas escribir $model.property sobre una propiedad no marcada con @Bindable, el compilador no podrá derivar el binding. Añade el wrapper en la propiedad o crea un @Bindable var local dentro del body.
  • Mutar propiedades fuera del hilo principal: aunque el macro es seguro en cuanto a memoria, las vistas SwiftUI esperan mutaciones en el MainActor. Anota la clase con @MainActor si va a actualizar UI.
  • No marcar la clase como final: @Observable funciona en clases que sí pueden heredarse, pero hay limitaciones con subclases. Cuando sea posible, marca tu view model como final para evitar sorpresas y dar al compilador más oportunidades de optimización.
  • Esperar que las estructuras (struct) se vuelvan observables: el macro solo funciona en clases. Los tipos valor siguen usando @State y @Binding directamente.

Preguntas frecuentes

¿Qué versión mínima de iOS necesita @Observable?

El macro @Observable requiere iOS 17, iPadOS 17, macOS 14 Sonoma, tvOS 17 o watchOS 10 como mínimo. Si tu app debe soportar iOS 16 o anteriores, mantén ObservableObject en esas clases o usa #available para bifurcar el código.

¿Puedo usar @Observable y ObservableObject en la misma app?

Sí. Ambas APIs coexisten sin conflictos, por lo que puedes migrar de forma incremental clase por clase. Lo único que no puedes hacer es mezclar ambos protocolos en una misma jerarquía de herencia: una clase es @Observable o ObservableObject, no las dos.

¿@Observable funciona con structs o solo con clases?

Solo con clases. El macro genera código que depende de semántica de referencia (un solo origen de verdad compartido entre vistas). Para tipos valor, SwiftUI ya ofrece @State y @Binding, que cubren el caso de uso sin necesidad de Observation.

¿Cómo observo cambios desde código que no es SwiftUI?

Usa la función pública withObservationTracking(_:onChange:) del framework Observation. Le pasas un closure que lee las propiedades a observar y otro que se invoca una vez cuando cualquiera de esas propiedades cambia. Para observación continua debes volver a llamarla dentro del closure onChange.

¿@Observable reemplaza a Combine?

No directamente. @Observable reemplaza el uso de Combine que estaba implícito en ObservableObject y @Published para el binding con SwiftUI, pero Combine sigue siendo útil para flujos de datos asíncronos complejos, debounce, throttling y composición de publicadores. Apple recomienda Swift Concurrency con async/await para nuevos desarrollos.

Editorial Team
Sobre el Autor Editorial Team

Our team of expert writers and editors.