Tutoriales de SwiftUI y Swift para iOS Developers - PROGRAMACIONSWIFT.COM

@Observable vs @Published en SwiftUI

Si has estado desarrollando aplicaciones para el ecosistema de Apple durante los últimos años, es probable que la gestión del estado haya sido tu principal fuente de dolores de cabeza. SwiftUI, en su concepción original, dependía fuertemente del framework Combine para comunicar cambios de datos a la interfaz de usuario. Palabras clave como ObservableObject@Published y @StateObject se convirtieron en el pan de cada día.

Sin embargo, con la llegada de Swift 5.9 (iOS 17, macOS 14, watchOS 10), Apple introdujo un cambio tectónico: el framework Observation y la macro @Observable.

Este tutorial no es solo una comparación superficial; es una inmersión técnica en las diferencias arquitectónicas, de rendimiento y de sintaxis entre el “Viejo Mundo” (@Published) y el “Nuevo Mundo” (@Observable). Aprenderás por qué Apple hizo este cambio y cómo puedes aprovecharlo para crear apps más rápidas y limpias en Xcode.


Parte 1: Entendiendo los Contendientes

Para entender las diferencias, primero debemos diseccionar cómo funciona cada sistema bajo el capó.

El Veterano: ObservableObject y @Published

En las primeras versiones de SwiftUI (iOS 13-16), la reactividad se basaba en el protocolo ObservableObject.

Cuando conformamos una clase a este protocolo, el compilador sintetiza un objectWillChange publisher (emisor de eventos). Pero, ¿cómo sabe este emisor cuándo dispararse? Aquí entra @Published.

@Published es un property wrapper. Envuelve tu propiedad y añade un gancho (hook) en el willSet. Cada vez que asignas un nuevo valor a una propiedad marcada con @Published, esta avisa al objeto padre: “¡Oye, voy a cambiar! Avisa a la vista”.

El problema fundamental: La granularidad. El aviso es a nivel de Objeto, no de propiedad.

El Retador: La Macro @Observable

El nuevo sistema utiliza las nuevas Macros de Swift. Cuando anotas una clase con @Observable, no estás usando un property wrapper en tiempo de ejecución. Estás instruyendo al compilador para que reescriba tu clase en tiempo de compilación.

La macro inyecta lógica en los getters y setters de todas las propiedades almacenadas. SwiftUI ya no “escucha” al objeto entero; en su lugar, crea un Grafo de Dependencias preciso. Si una vista lee una propiedad, se suscribe a esapropiedad específica.


Parte 2: Diferencias Críticas de Arquitectura

Aquí es donde entramos en materia técnica. La diferencia entre ambos no es solo sintaxis; es comportamiento.

1. El Problema de la Invalidación Excesiva (Over-invalidation)

Esta es la razón número uno por la que Apple creó @Observable.

Escenario con @Published: Imagina un UserProfileViewModel con dos propiedades:

class UserProfile: ObservableObject {
    @Published var username: String = "Dev"
    @Published var heartRate: Int = 60 // Cambia 100 veces por minuto
}

Y tienes una vista que solo muestra el nombre:

struct NameView: View {
    @ObservedObject var model: UserProfile
    var body: some View {
        Text(model.username)
    }
}

Lo que sucede: Cada vez que heartRate cambia (aunque la vista no lo use), objectWillChange se dispara. SwiftUI invalida NameView, calcula el body, compara el resultado y se da cuenta de que nada visual cambió. Esto es un desperdicio masivo de ciclos de CPU.

Escenario con @Observable:

@Observable class UserProfile {
    var username: String = "Dev"
    var heartRate: Int = 60
}

Lo que sucede: Cuando NameView se renderiza por primera vez, lee model.username. El sistema de observación registra: “NameView depende de UserProfile.username”. Si heartRate cambia, el sistema mira el grafo de dependencias, ve que nadie está observando heartRate en esta vista, y no hace nada. La vista ni siquiera se entera.

2. Sintaxis y “Boilerplate”

La reducción de ruido visual es drástica. Veamos una comparación directa de código.

El Viejo Estilo (Combine)

import SwiftUI

class SettingsModel: ObservableObject {
    @Published var isDarkMode: Bool = false
    @Published var volume: Double = 0.5
    
    // Las propiedades calculadas no emiten cambios automáticamente
    // a menos que dependan de propiedades @Published
}

struct SettingsView: View {
    @StateObject private var model = SettingsModel()
    
    var body: some View {
        Toggle("Dark Mode", isOn: $model.isDarkMode)
    }
}

El Nuevo Estilo (Observation)

import SwiftUI
import Observation // Importante si no importas SwiftUI

@Observable 
class SettingsModel {
    var isDarkMode: Bool = false
    var volume: Double = 0.5
    // No hace falta marcar nada como @Published
}

struct SettingsView: View {
    // Adiós @StateObject, hola @State
    @State private var model = SettingsModel()
    
    var body: some View {
        // Enlace directo
        Toggle("Dark Mode", isOn: $model.isDarkMode)
    }
}

3. Inyección de Dependencias (El fin de la “Sopa de Wrappers”)

Con @Published, teníamos reglas estrictas y confusas sobre qué wrapper usar en la vista:

  • Creación: @StateObject (para que el objeto sobreviva a redibujados).
  • Observación: @ObservedObject (si alguien más lo creó).
  • Entorno: @EnvironmentObject.

Si te equivocabas y usabas @ObservedObject donde debías usar @StateObject, tu modelo se reiniciaba aleatoriamente.

Con @Observable, todo se simplifica:

  • Creación: Usa @State. Sí, @State ahora gestiona el ciclo de vida de objetos observables.
  • Observación: Usa let o var normales. No necesitas un wrapper. SwiftUI detecta automáticamente que es un objeto observable.
  • Bindings: Usa @Bindable si necesitas el símbolo $ (explicaremos esto más adelante).
  • Entorno: Usa @Environment(TuClase.self).

Parte 3: Profundizando en las Propiedades Calculadas

Una de las diferencias más sutiles pero poderosas está en cómo se manejan las propiedades calculadas (computed properties).

Con @Published: Si tenías una propiedad calculada que dependía de otras variables, @Published funcionaba bien siempre y cuando las dependencias también fueran @Published. Pero si tu propiedad calculada dependía de algo externo o complejo, a menudo tenías que llamar manualmente a objectWillChange.send().

Con @Observable: El sistema es inteligente. Rastrea las dependencias de forma recursiva.

@Observable class Cart {
    var items: [Item] = []
    
    // Esta propiedad es observada automáticamente
    var total: Double {
        items.reduce(0) { $0 + $1.price }
    }
}

Si añades un item al array, total cambia. Cualquier vista que muestre total se actualizará. No necesitas código extra.


Parte 4: El papel de @Bindable (El nuevo jugador)

Aquí es donde muchos desarrolladores se confunden al migrar.

En el viejo sistema, @ObservedObject proyectaba un Binding automáticamente (vía $modelo). En el nuevo sistema, como pasamos el objeto como un let o var simple, no tenemos esa proyección automática.

Si necesitas pasar un valor a un TextField o un Toggle (que requieren escritura), necesitas usar el wrapper @Bindable.

Tutorial de uso de @Bindable:

  1. En la raíz: Creas el objeto con @State. Accedes al binding con $.
@State private var user = User()
TextField("Nombre", text: $user.name) // Funciona


2. En una vista hija: Recibes el objeto “crudo”.

struct EditView: View {
    var user: User // Recibido normal

    var body: some View {
        // Error: No puedes usar $user
        // TextField("Nombre", text: $user.name) 

        // Solución: Crear un Bindable al vuelo
        @Bindable var bUser = user
        TextField("Nombre", text: $bUser.name)
    }
}

También puedes declarar @Bindable var user: User en los argumentos de la vista si siempre vas a necesitar bindings.


Parte 5: Rendimiento y Colecciones

El manejo de colecciones (Arrays de objetos) siempre fue un punto doloroso con ObservableObject. Si tenías un Array de objetos observables, cambiar una propiedad dentro de uno de esos objetos no actualizaba la vista que mostraba la lista, a menos que invalidaras el array manualmente.

Con @Observable, la propagación es mucho más fluida, pero requiere entender que la observación ocurre en el acceso.

Si tienes: var users: [User] (donde User es @Observable)

Y tu vista hace:

ForEach(users) { user in
    Text(user.name)
}

SwiftUI rastrea:

  1. El array users (si añades/borras elementos).
  2. La propiedad name de cada instancia User visible.

Esto permite listas enormemente eficientes donde actualizar el nombre del usuario #50 solo repinta esa celda específica, sin tocar el resto de la lista ni la estructura del contenedor.


Parte 6: Guía de Migración Paso a Paso

¿Tienes una app grande en Xcode con ObservableObject? No entres en pánico. No necesitas reescribirlo todo hoy.

Estrategia de Convivencia

Ambos sistemas pueden coexistir en la misma app, pero no en la misma vista/objeto.

  1. No mezcles: Un objeto no puede ser ObservableObject y @Observable a la vez.
  2. Vistas Híbridas: Una vista puede tener un @StateObject (viejo) y un @State (nuevo) de dos modelos diferentes. Funcionará.

Pasos para Refactorizar un Modelo

  1. Cambia la declaración:
    • Elimina : ObservableObject.
    • Añade @Observable antes de class.
    • Importa Observation.
  2. Limpia las propiedades:
    • Elimina todos los @Published. Simplemente déjalos como var.
  3. Actualiza las Vistas:
    • Cambia @StateObject var model = ... por @State var model = ....
    • Cambia @ObservedObject var model: Tipo por let model: Tipo (o var).
    • Si usas bindings ($model.propiedad), añade @Bindable var model en el cuerpo de la vista o en la declaración.
    • Cambia @EnvironmentObject por @Environment(Tipo.self).

Parte 7: ¿Cuándo NO usar @Observable?

A pesar de todas las ventajas, hay un muro insalvable: La compatibilidad.

El framework Observation solo está disponible en:

  • iOS 17.0+
  • macOS 14.0+
  • watchOS 10.0+
  • tvOS 17.0+

Si tu cliente o tu empresa requieren soporte para iOS 15 o iOS 16no puedes usar @Observable (a menos que uses #available y mantengas dos versiones del código, lo cual no suele ser práctico para la capa de modelo).

En este caso, @Published y ObservableObject siguen siendo herramientas perfectamente válidas, estables y soportadas por Apple. No están “deprecadas” en el sentido de que vayan a desaparecer pronto, simplemente son la tecnología “legacy”.


Parte 8: Resumen Técnico

Para cerrar este tutorial, aquí tienes una tabla de referencia rápida para tener a mano en tu escritorio mientras programas en Xcode.

CaracterísticaObservableObject (@Published)@Observable (Macro)
MecanismoCombine (Publisher/Subscriber)Swift Runtime & Macros
GranularidadObjeto entero (Cualquier cambio notifica)Propiedad individual (Solo lo usado notifica)
Declaraciónclass A: ObservableObject@Observable class A
Marcar Propiedades@Published var xvar x (Automático)
Ignorar Propiedadesvar x (Sin @Published)@ObservationIgnored var x
En la Vista (Dueño)@StateObject@State
En la Vista (Lector)@ObservedObjectlet / var
BindingsAutomático ($objeto.prop)Manual (@Bindable o Bindable())
Requisito MínimoiOS 13iOS 17

Conclusión

La transición de @Published a @Observable representa la madurez de SwiftUI. Apple ha eliminado la dependencia de Combine para la capa de la vista, reconociendo que el modelo reactivo de flujos (Streams) no siempre es el mejor modelo para el estado de la interfaz (State).

@Observable es más “Swift-nativo”. Se siente menos como una librería añadida y más como una característica intrínseca del lenguaje. Reduce el código repetitivo, elimina errores comunes de rendimiento y simplifica la enseñanza de SwiftUI a nuevos desarrolladores.

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

Visited 1 times, 1 visit(s) today
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

@Observable en SwiftUI para iOS, macOS y watchOS

Next Article

Picker en SwiftUI

Related Posts