Historia de la pregunta
Antes de Swift 5.9, la programación reactiva en SwiftUI dependía del protocolo ObservableObject combinado con el marco Combine. Los desarrolladores anotaban manualmente las propiedades con @Published para sintetizar publicadores, o llamaban a objectWillChange.send() para notificar a las vistas sobre las mutaciones. Este patrón sufría de actualizaciones de grano grueso: cualquier cambio de propiedad desencadenaba una reevaluación completa del cuerpo de la vista—y requería semántica de referencia, lo que impedía el uso de structs para modelos de vista complejos. Se introdujo el marco de Observación para proporcionar reactividad automática y de grano fino sin declaraciones explícitas de publicadores.
El problema
El desafío principal era detectar el acceso y la mutación de propiedades sin un exceso de código explícito, mientras se mantenía la seguridad de tipos y un alto rendimiento. Las soluciones tradicionales requerían observación manual de clave-valor (KVO), que es frágil y propensa a errores de tipificación, o tipos envolventes que sobrecargaban el modelo de dominio. El sistema necesitaba interceptar operaciones de lectura y escritura para registrar dinámicamente las dependencias, pero sin el costo en tiempo de ejecución de la interpolación de métodos o las limitaciones arquitectónicas de la propagación de identidad contada por referencias de ObservableObject.
La solución
Swift emplea la macro @Observable, que combina capacidades de macros de pares, miembros y accesores. Durante la compilación, la macro transforma la clase anotada inyectando una instancia privada de ObservationRegistrar. Luego reescribe cada propiedad almacenada en accesores computados que envuelven lecturas con _$observationRegistrar.track(self, keyPath: ...) y escrituras con notificaciones willSet/didSet. Esta expansión sintetiza automáticamente la conformidad al protocolo Observable y implementa la propiedad computada necesaria observationRegistrar. SwiftUI se integra con este registrador durante la evaluación del cuerpo de la vista, registrándose como observador solo para las propiedades realmente accedidas, logrando así actualizaciones granulares sin configuración manual de Combine.
@Observable class SettingsViewModel { var isDarkModeEnabled = false var notificationCount = 0 } // Expansión conceptual del compilador: class SettingsViewModel { private let _$observationRegistrar = ObservationRegistrar() var isDarkModeEnabled: Bool { get { _$observationRegistrar.track(self, keyPath: \.isDarkModeEnabled) return _isDarkModeEnabled } set { _$observationRegistrar.willSet(self, keyPath: \.isDarkModeEnabled) let oldValue = _isDarkModeEnabled _isDarkModeEnabled = newValue _$observationRegistrar.didSet(self, keyPath: \.isDarkModeEnabled, oldValue: oldValue) } } private var _isDarkModeEnabled = false // ... patrón idéntico para notificationCount }
Estás arquitectando una aplicación de panel financiero en tiempo real de SwiftUI que muestra precios de acciones en vivo, totales de cartera de usuario y feeds de noticias. El modelo de vista contiene treinta propiedades distintas, que varían desde indicadores Booleanos de UI hasta estructuras de datos de gráficos complejos.
Inicialmente, el equipo implementó esto usando ObservableObject con envoltorios @Published en cada propiedad. Esto causó una fuerte degradación del rendimiento: cuando se actualizaba un solo precio de acción, todo el panel se recalculaba porque ObservableObject notifica que "algo cambió" en todo el objeto, careciendo de granularidad. El código también era extenso, requiriendo declaraciones repetitivas de @Published var y almacenamiento manual de AnyCancellable para prevenir fugas de memoria en suscripciones.
El equipo evaluó tres enfoques arquitectónicos para resolver los problemas de rendimiento y exceso de código.
El primer enfoque implicaba optimización manual de Combine. Crearían instancias individuales de PassthroughSubject para cada propiedad crítica y se suscribirían a actualizaciones específicas usando .onReceive. La ventaja era el control preciso sobre qué componentes de UI se actualizaban. Sin embargo, la desventaja era el enorme aumento de código—treinta sujetos requerían treinta suscripciones y gestión de memoria manual propensa a errores con Set<AnyCancellable>, lo que hacía que la base de código fuese inmantenible.
El segundo enfoque sugería usar @State de SwiftUI con modelos de vista de tipo valor. Tratarían el modelo de vista como un valor inmutable y lo reemplazarían en cada mutación. La ventaja era la semántica de valor natural y las verificaciones de igualdad automáticas que prevenían actualizaciones redundantes. La desventaja era la pérdida de identidad de referencia; cada mutación creaba una nueva instancia, rompiendo la restauración de la posición de ScrollView y haciendo imposible la coordinación de objetos complejos debido a la pérdida de identidad.
El tercer enfoque adoptó la macro @Observable. Al anotar la clase con @Observable y eliminar todos los atributos @Published, el compilador transformó automáticamente las propiedades para usar el ObservationRegistrar. La ventaja era doble: la sintaxis se mantenía limpia con declaraciones simples de var, y SwiftUI seguía automáticamente qué propiedades se accedían en el cuerpo de cada vista, actualizando solo esos subcomponentes específicos. La desventaja era el requisito de migrar a Swift 5.9 y capacitar al equipo sobre técnicas de depuración de macros.
El equipo eligió el tercer enfoque porque eliminó 200 líneas de código de suscripción de Combine mientras resolvía el problema de granularidad. Observaron una reducción del 40% en el uso de CPU durante actualizaciones de precios de alta frecuencia. El resultado fue un panel receptivo donde las etiquetas de precios de acciones individuales se actualizaban independientemente sin provocar recalculos de diseño para la sección estática de feed de noticias.
¿Por qué requiere el marco de Observación que el tipo observado sea una clase (semántica de referencia), y qué error de compilación ocurre si se aplica @Observable a un struct?
La macro @Observable se expande inyectando un ObservationRegistrar como una propiedad almacenada e implementando el protocolo Observable. Los structs son tipos de valor con semántica de copiado en escritura; cada mutación crea conceptualmente una nueva instancia con una identidad distinta. El ObservationRegistrar mantiene el estado interno—listas de observadores y ámbitos de seguimiento—que deben persistir a través de mutaciones para mantener el gráfico de observación. Si se aplica a un struct, las mutaciones copiarían incorrectamente el estado del registrador, rompiendo la conexión entre los observadores y la instancia. El compilador previene esto generando un error que indica que la macro no puede agregar la propiedad almacenada requerida a un tipo de valor de una manera que satisfaga los requisitos del protocolo Observable para una identidad estable, o más específicamente, que el tipo resultante no puede conformarse a Observable porque carece de la estabilidad de referencia necesaria.
¿Cómo maneja el marco de Observación los objetos observables anidados, y por qué no hay un valor proyectado (como $property) para propiedades individuales como había con @Published?
Cuando una clase @Observable contiene una propiedad que es a su vez una clase @Observable, el marco rastrea el acceso a nivel de propiedad, no observando automáticamente objetos anidados de manera recursiva. Acceder a outer.inner.name registra una dependencia sobre la propiedad inner del objeto externo. Si la instancia inner se reemplaza totalmente, se notifican a los observadores. Sin embargo, los cambios en inner.name no notifican a los observadores del objeto externo a menos que el objeto externo rastree explícitamente el interno. A diferencia de Combine, no hay un concepto de valor proyectado para propiedades individuales en Observación porque el marco utiliza seguimiento directo de propiedades a través del registrador, en lugar de flujos de publicadores. La sintaxis $ en SwiftUI para Observación se utiliza en cambio en toda la instancia cuando se envuelve con @Bindable (por ejemplo, @Bindable var viewModel: SettingsViewModel permite $viewModel.isDarkModeEnabled), no en declaraciones de propiedades individuales.
¿Qué garantías específicas de seguridad de subprocesos proporciona el marco de Observación y cómo interactúa con los actores de concurrencia de Swift?
El ObservationRegistrar en sí mismo no es inherentemente seguro para subprocesos; asume el acceso serializado al objeto observable. Cuando una clase @Observable está aislada a un actor (como @MainActor), todas las mutaciones y observaciones ocurren automáticamente en el contexto de ese actor, previniendo condiciones de competencia. El marco asegura que las devoluciones de llamada de observación respeten el dominio de aislamiento del observador utilizando verificaciones Sendable. Un detalle crítico de implementación es que el mecanismo de seguimiento utiliza almacenamiento TaskLocal para mantener el ámbito de observación actual durante la ejecución del cuerpo de la vista. Esto significa que el registro de observación está implícitamente vinculado al contexto de la Task actual y no puede filtrarse a través de fronteras de concurrencia no estructuradas sin transferencia explícita, asegurando que las observaciones solo estén activas durante la transacción asincrónica específica en la que fueron registradas.