Storia della domanda
Prima di Swift 5.9, la programmazione reattiva in SwiftUI si basava sul protocollo ObservableObject combinato con il framework Combine. Gli sviluppatori annotavano manualmente le proprietà con @Published per sintetizzare i publisher, o chiamavano objectWillChange.send() per notificare le viste delle mutazioni. Questo schema soffriva di aggiornamenti grossolani: qualsiasi modifica di proprietà triggerava una rivalutazione completa del corpo della vista, e imponeva semantiche di riferimento, impedendo l'uso di strutture per modelli di vista complessi. Il framework Observation è stato introdotto per fornire reattività automatica e a grana fine senza dichiarazioni di publisher esplicite.
Il problema
La sfida principale era rilevare l'accesso e la mutazione delle proprietà senza boilerplate esplicito, mantenendo al contempo la sicurezza dei tipi e alte prestazioni. Le soluzioni tradizionali richiedevano l'osservazione manuale delle chiavi (KVO), che è tipizzata tramite stringhe e fragile, o tipi wrapper che ingombravano il modello di dominio. Il sistema doveva intercettare le operazioni di lettura e scrittura per registrare le dipendenze dinamicamente, ma senza il sovraccarico a tempo di esecuzione dell'hook dei metodi o le limitazioni architettoniche della propagazione dell'identità contata di riferimento di ObservableObject.
La soluzione
Swift utilizza la macro @Observable, che combina capacità macro peer, membro e accessor. Durante la compilazione, la macro trasforma la classe annotata iniettando un'istanza privata di ObservationRegistrar. Quindi riscrive ogni proprietà memorizzata in accessori calcolati che avvolgono le letture con _$observationRegistrar.track(self, keyPath: ...) e le scritture con notifiche willSet/didSet. Questa espansione sintetizza automaticamente la conformità al protocollo Observable e implementa la necessaria proprietà calcolata observationRegistrar. SwiftUI si integra con questo registrar durante la valutazione del corpo della vista, registrandosi come osservatore solo per le proprietà effettivamente accessibili, raggiungendo così aggiornamenti granulari senza configurazione manuale di Combine.
@Observable class SettingsViewModel { var isDarkModeEnabled = false var notificationCount = 0 } // Espansione concettuale del compilatore: 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 // ... modello identico per notificationCount }
Stai architettando un'applicazione SwiftUI per un cruscotto finanziario in tempo reale che mostra i prezzi delle azioni live, i totali del portafoglio utente e i feed di notizie. Il modello di vista contiene trenta proprietà distinte, che vanno da flag UI booleani a strutture di dati complessi per grafici.
Inizialmente, il team ha implementato questo utilizzando ObservableObject con wrapper @Published su ogni proprietà. Questo ha causato gravi degradi delle prestazioni: quando un solo prezzo di azione si aggiornava, l'intero cruscotto veniva ricalcolato perché ObservableObject notifica che "qualcosa è cambiato" nell'intero oggetto, mancando di granularità. Il codice era anche verboso, richiedendo ripetitive dichiarazioni @Published var e memorizzazione manuale di AnyCancellable per prevenire perdite di memoria nelle sottoscrizioni.
Il team ha valutato tre approcci architettonici per risolvere i problemi di prestazioni e boilerplate.
Il primo approccio prevedeva l'ottimizzazione manuale di Combine. Creerebbero istanze individuali di PassthroughSubject per ciascuna proprietà critica e si iscrivono a specifici aggiornamenti utilizzando .onReceive. Il vantaggio era un controllo preciso su quali componenti UI venivano aggiornati. Tuttavia, lo svantaggio era l'enorme ingombro del codice: trenta soggetti richiedevano trenta sottoscrizioni e gestione manuale della memoria soggetta a errori con Set<AnyCancellable>, rendendo il codice non mantenibile.
Il secondo approccio suggeriva di utilizzare @State di SwiftUI con modelli di vista di tipo valore. Avrebbero trattato il modello di vista come un valore immutabile e lo avrebbero sostituito a ogni mutazione. Il vantaggio era la naturale semantica di valore e i controlli di uguaglianza automatici che prevenivano aggiornamenti ridondanti. Lo svantaggio era la perdita dell'identità del riferimento; ogni mutazione creava una nuova istanza, rompendo il ripristino della posizione di ScrollView e rendendo impossibile la coordinazione di oggetti complessi a causa della perdita dell'identità.
Il terzo approccio adottava la macro @Observable. Annotando la classe con @Observable e rimuovendo tutti gli attributi @Published, il compilatore trasformava automaticamente le proprietà per utilizzare l'ObservationRegistrar. Il vantaggio era duplice: la sintassi rimaneva pulita con semplici dichiarazioni var, e SwiftUI tracciava automaticamente quali proprietà erano accessibili nel corpo di ciascuna vista, aggiornando solo quelle sotto-vista specifiche. Lo svantaggio era la necessità di migrarsi a Swift 5.9 e formare il team sulle tecniche di debug delle macro.
Il team ha scelto il terzo approccio perché ha eliminato 200 righe di codice di sottoscrizione Combine risolvendo al contempo il problema della granularità. Hanno osservato una riduzione del 40% dell'uso della CPU durante gli aggiornamenti ad alta frequenza dei prezzi. Il risultato è stato un cruscotto reattivo in cui le etichette del prezzo delle azioni individuali venivano aggiornate indipendentemente senza attivare ricalcoli del layout per la sezione di feed di notizie statica.
Perché il framework Observation richiede che il tipo osservato sia una classe (semantiche di riferimento), e quale errore di compilazione si verifica se @Observable viene applicato a una struttura?
La macro @Observable si espande iniettando un ObservationRegistrar come proprietà memorizzata e implementando il protocollo Observable. Le strutture sono tipi valore con semantiche di copia al momento della scrittura; ogni mutazione crea concettualmente una nuova istanza con un'identità distinta. L'ObservationRegistrar mantiene uno stato interno—liste di osservatori e ambiti di tracciamento—che deve persistere attraverso le mutazioni per mantenere il grafo di osservazione. Se applicato a una struttura, le mutazioni copierebbero lo stato del registrar in modo errato, rompendo la connessione tra osservatori e istanza. Il compilatore previene questo generando un errore che indica che la macro non può aggiungere la proprietà memorizzata richiesta a un tipo valore in un modo che soddisfi i requisiti del protocollo Observable per un'identità stabile, o più specificamente, che il tipo risultante non può conformarsi a Observable perché manca della necessaria stabilità di riferimento.
Come gestisce il framework Observation gli oggetti osservabili annidati, e perché non esiste un valore previsto (come $property) per singole proprietà come c'era con @Published?
Quando una classe @Observable contiene una proprietà che è essa stessa una classe @Observable, il framework tiene traccia dell'accesso a livello di proprietà, non osservando automaticamente oggetti annidati. Accedere a outer.inner.name registra una dipendenza sulla proprietà inner dell'oggetto esterno. Se l'istanza inner viene completamente sostituita, gli osservatori vengono notificati. Tuttavia, le modifiche a inner.name non notificano gli osservatori dell'oggetto esterno a meno che l'oggetto esterno non tracci esplicitamente l'interno. A differenza di Combine, non esiste un concetto di valore previsto per singole proprietà in Observation perché il framework utilizza il tracciamento diretto delle proprietà tramite il registrar piuttosto che flussi di publisher. La sintassi $ in SwiftUI per Observation è invece utilizzata su tutta l'istanza quando avvolta con @Bindable (ad es., @Bindable var viewModel: SettingsViewModel consente $viewModel.isDarkModeEnabled), non sulle dichiarazioni di singole proprietà.
Quali specifiche garanzie di sicurezza dei thread fornisce il framework Observation, e come interagisce con gli attori di concorrenza di Swift?
L'ObservationRegistrar non è intrinsecamente thread-safe; assume un accesso serializzato all'oggetto osservabile. Quando una classe @Observable è isolata a un attore (come @MainActor), tutte le mutazioni e le osservazioni avvengono automaticamente nel contesto di quel attore, prevenendo le condizioni di competizione. Il framework garantisce che i callback di osservazione rispettino il dominio di isolamento dell'osservatore utilizzando controlli Sendable. Un dettaglio critico di implementazione è che il meccanismo di tracciamento utilizza la memorizzazione TaskLocal per mantenere l'ambito di osservazione corrente durante l'esecuzione del corpo della vista. Ciò significa che la registrazione delle osservazioni è implicitamente legata al contesto dell'attuale Task e non può fuoriuscire attraverso confini di concorrenza non strutturati senza un trasferimento esplicito, garantendo che le osservazioni siano attive solo durante la specifica transazione asincrona in cui sono state registrate.