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.
¿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 uso | ObservableObject (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 | @ObservedObject | propiedad normal (let / var) |
| Inyectar en el entorno | @EnvironmentObject + .environmentObject(_:) | @Environment + .environment(_:) |
| Pasar binding a control | $viewModel.property (directo) | @Bindable + $model.property |
| Excluir propiedad del rastreo | omitir @Published | @ObservationIgnored |
| Granularidad de invalidación | objeto completo | por propiedad leída |
| Compatibilidad mínima | iOS 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:
- Importa
Observationen el archivo (Swift lo añade automáticamente si usas Xcode 16+). - Elimina la conformidad
: ObservableObjectde la clase y aplica el macro@Observable. - Elimina todas las anotaciones
@Publishedde las propiedades almacenadas. - En las vistas, sustituye
@StateObjectpor@State, elimina@ObservedObjectde propiedades pasadas como dependencia, y reemplaza@EnvironmentObjectpor@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: Usercuando la instancia se inyecta como dependencia. - Como variable local dentro del
body:@Bindable var model = modelcuando la instancia vino por@Stateo@Environmenty 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:
- 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.
- Modelos con muchas propiedades: una pantalla de formulario con veinte campos antes redibujaba el formulario entero al teclear; ahora cada
TextFieldes independiente. - Jerarquías profundas con
@EnvironmentObject: el viejo modelo invalidaba todas las vistas que leían el entorno, aunque ignorasen el campo cambiado.@Environmentcon un tipo@Observablesolo 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 delbodyy pierdes todo el estado. Es el error más frecuente al migrar desde@StateObject. - Olvidar
@Bindableal pasar bindings: si intentas escribir$model.propertysobre una propiedad no marcada con@Bindable, el compilador no podrá derivar el binding. Añade el wrapper en la propiedad o crea un@Bindable varlocal dentro delbody. - 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@MainActorsi va a actualizar UI. - No marcar la clase como
final:@Observablefunciona en clases que sí pueden heredarse, pero hay limitaciones con subclases. Cuando sea posible, marca tu view model comofinalpara 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@Statey@Bindingdirectamente.
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.




