Macro @Observable in SwiftUI: Guida Completa per iOS 26
Guida pratica alla macro @Observable del framework Observation: differenze con ObservableObject, migrazione, @Bindable, @Environment e withObservationTracking, con esempi Swift 6.2 e Xcode 26.
La macro @Observable in SwiftUI è un attributo che, applicato a una classe, genera automaticamente il codice necessario per tracciare l'accesso alle singole proprietà, permettendo a SwiftUI di aggiornare solo le viste che leggono davvero un valore modificato. Introdotta con iOS 17 e il framework Observation, sostituisce il vecchio protocollo ObservableObject eliminando @Published, @StateObject e @EnvironmentObject. In iOS 26 e Swift 6.2 è il modo consigliato per gestire lo stato condiviso: più veloce, più preciso e con parecchio meno codice ripetitivo.
La macro @Observable traccia gli accessi a livello di singola proprietà: SwiftUI ridisegna solo le viste che leggono davvero il valore modificato.
Richiede iOS 17+, macOS 14+, tvOS 17+, watchOS 10+ e Swift 5.9 o superiore; con Swift 6.2 e Xcode 26 si integra con l'isolamento MainActor predefinito.
Sostituisce @Published, @StateObject e @EnvironmentObject: nelle viste si usa @State, @Environment o @Bindable.
@Bindable serve a creare binding a due vie verso una classe @Observable; per struct ed enum si continua a usare @Binding.
Fuori da SwiftUI si osservano le modifiche con withObservationTracking, ma la callback onChange viene invocata una sola volta: va ripianificata ricorsivamente.
Le proprietà che non devono partecipare al tracciamento (cache, timer, dipendenze) vanno annotate con @ObservationIgnored.
Cos'è la macro @Observable in SwiftUI
La macro @Observable è una macro di attaccamento (attached macro) dichiarata nel framework Observation, introdotta a WWDC 2023 e diventata l'unico modello consigliato con Xcode 26 e Swift 6.2. Quando la applichiamo a una classe, il compilatore inietta a nostro nome la conformità al protocollo Observable, avvolge ogni proprietà memorizzata con un getter/setter tracciato e registra le letture nel contesto di osservazione attivo.
Chi come me ha attraversato la transizione da Objective-C a Swift ricorda benissimo la stanchezza di scrivere -setNeedsDisplay a mano, o di annotare ogni @dynamic per KVO. Il framework Observation prende quell'idea (sapere cosa è cambiato) e la porta al livello del compilatore Swift, senza rinunciare alla sicurezza dei tipi.
La sintassi è disarmante:
import Observation
@Observable
final class Counter {
var value = 0
var history: [Int] = []
func increment() {
history.append(value)
value += 1
}
}
Nessun @Published, nessuna conformità a ObservableObject. La classe è una normale final class e SwiftUI la userà come qualsiasi altro valore osservabile. All'interno di una vista basta un @State:
struct CounterView: View {
@State private var counter = Counter()
var body: some View {
VStack {
Text("Valore: \(counter.value)")
Button("Incrementa") { counter.increment() }
}
}
}
Il dettaglio importante è che @State ora funziona anche per reference type, purché siano @Observable. La distinzione storica tra @State per i valori e @StateObject per gli oggetti sparisce: la posseduta è la vista, punto.
Qual è la differenza tra @Observable e ObservableObject
La differenza sostanziale non è cosmetica: cambia il modello mentale di ciò che scatena un aggiornamento. Con ObservableObject, un cambio a una qualsiasi proprietà pubblicata invalida l'intera vista che osserva l'oggetto. Con @Observable, solo le viste che hanno letto quella specifica proprietà nel proprio body vengono ridisegnate. In un'app di medie dimensioni la differenza in fluidità è tangibile.
Caratteristica
ObservableObject
@Observable
Anno di introduzione
iOS 13 (2019)
iOS 17 (2023)
Granularità del tracciamento
Oggetto intero
Singola proprietà
Annotazione richiesta
@Published su ogni proprietà
Nessuna
Property wrapper nelle viste
@StateObject, @ObservedObject, @EnvironmentObject
Solo @State, @Environment, @Bindable
Dipende da Combine
Sì (ObservableObjectPublisher)
No, framework autonomo
Compatibilità minima
iOS 13, macOS 10.15
iOS 17, macOS 14
Osservazione fuori da SwiftUI
Subscribe manuale al publisher
withObservationTracking
Un altro punto silenziosamente opinato: ObservableObject costringeva a un design in cui ogni valore da mostrare doveva essere @Published, il che rendeva difficile distinguere lo stato di rendering dalle dipendenze private. Con @Observable tutte le proprietà sono osservate per default e si esclude esplicitamente ciò che non serve tracciare. Un default molto più sano, onestamente.
Come migrare da ObservableObject a @Observable
La migrazione è meccanica ma va fatta con attenzione. Prendiamo un tipico ViewModel legacy:
import Combine
final class ProfileViewModel: ObservableObject {
@Published var username: String = ""
@Published var isLoading: Bool = false
@Published var errorMessage: String?
private var cancellables = Set<AnyCancellable>()
}
La versione Observation-first diventa:
import Observation
@Observable
final class ProfileViewModel {
var username: String = ""
var isLoading: Bool = false
var errorMessage: String?
@ObservationIgnored
private var cancellables = Set<AnyCancellable>()
}
I passi da eseguire, in ordine:
Aggiungere import Observation e sostituire la conformità ObservableObject con la macro @Observable.
Rimuovere ogni @Published: le proprietà restano var normali.
Annotare con @ObservationIgnored tutto ciò che non deve triggerare aggiornamenti (cache, cancellables Combine, servizi iniettati, timer).
Nelle viste, cambiare @StateObject var vm = … in @State private var vm = ….
Cambiare @ObservedObject var vm: … in una property normale var vm: ProfileViewModel.
Cambiare @EnvironmentObject in @Environment(ProfileViewModel.self).
Rimuovere $vm.username lì dove serviva un binding: userai @Bindable, mostrato nel prossimo paragrafo.
Onestamente, per ridurre il rischio in codebase grandi io preferisco una migrazione a strati: mantengo i ViewModel esistenti e adotto @Observable solo nelle feature nuove. È la stessa strategia che ho descritto quando parlavamo di SwiftData e migrazione dei modelli, e in un progetto reale mi ha risparmiato settimane di regressioni impreviste. Il team prende confidenza gradualmente e le PR restano piccole.
@Bindable e i binding a due vie
Con ObservableObject, il prefisso $ su una proprietà @Published restituiva un Binding. Con @Observable serve un passaggio esplicito: @Bindable. È il property wrapper che dice a SwiftUI «da questa istanza voglio poter ricavare binding sulle sue proprietà».
struct LoginForm: View {
@Bindable var viewModel: LoginViewModel
var body: some View {
Form {
TextField("Email", text: $viewModel.email)
SecureField("Password", text: $viewModel.password)
Toggle("Ricordami", isOn: $viewModel.rememberMe)
}
}
}
Quando LoginForm riceve viewModel dall'esterno, non usare @State: quello segnala proprietà. Usa @Bindable se ti servono i binding, altrimenti una var nuda.
Un pattern che uso spesso è il bindable inline, utile quando l'oggetto arriva dall'ambiente e non si vuole dichiararlo due volte:
struct ProfileScreen: View {
@Environment(AuthViewModel.self) private var auth
var body: some View {
@Bindable var auth = auth
TextField("Nome utente", text: $auth.username)
}
}
La sintassi sembra ridondante, ma è intenzionale: dichiara localmente una nuova view del valore che espone $auth. Per i binding verso struct, enum o attori si continua a usare @Binding. @Bindable serve solo con classi @Observable.
@Environment: il nuovo pattern per l'iniezione
L'idea di iniettare un oggetto in tutto l'albero delle viste rimane, ma la sintassi cambia. Il vecchio @EnvironmentObject è deprecato in favore di @Environment con un tipo Observable. La differenza chiave: SwiftUI usa il tipo come chiave, non un protocollo generico. Questo permette di iniettare più istanze dello stesso tipo, cosa che @EnvironmentObject non consentiva.
@Observable
final class Theme {
var accent: Color = .indigo
var isDark: Bool = false
}
@main
struct MyApp: App {
@State private var theme = Theme()
var body: some Scene {
WindowGroup {
RootView()
.environment(theme)
}
}
}
struct SettingsRow: View {
@Environment(Theme.self) private var theme
var body: some View {
Text("Tema attivo")
.foregroundStyle(theme.accent)
}
}
Se il valore può essere assente, dichiara la proprietà come opzionale (@Environment(Theme.self) private var theme: Theme?) e gestiscine la mancanza. Con Xcode 26 questa scelta è particolarmente utile in combinazione con anteprime SwiftUI che iniettano tema e sessione utente da un piccolo container di preview.
Osservare modifiche fuori da SwiftUI con withObservationTracking
SwiftUI traccia automaticamente le letture nel body, ma spesso serve reagire a un cambiamento in un layer inferiore: logger, analytics, sincronizzazione con un file su disco, aggiornamenti a un modello Metal. Per quei casi il framework espone withObservationTracking(_:onChange:).
Ci sono due dettagli che il 90% delle guide superficiali non menziona. Su questi ho perso un pomeriggio in un progetto reale, quindi presta attenzione:
onChange viene invocata una sola volta. Se vuoi continuare a osservare devi ripianificare la registrazione. La ricorsione dentro un Task è il pattern canonico.
onChange è chiamata prima che il valore venga scritto. Se leggessi counter.value dentro onChange vedresti il vecchio valore. Il rinvio con Task { ... } non è cosmesi: è ciò che ti fa vedere lo stato aggiornato.
Il codice sorgente di riferimento è pubblico: si trova nel repository swiftlang/swift, sotto stdlib/public/Observation. Leggere ObservationTracking.swift chiarisce perché il framework è thread-safe e come funziona il registro delle letture. Utile se stai integrando osservazione con framework di test come Swift Testing su Xcode 26, dove il determinismo dell'aggiornamento fa la differenza.
@ObservationIgnored e oggetti annidati
Non tutto ciò che vive in una classe @Observable deve essere osservato. Ad esempio: connessioni di rete, servizi iniettati, cache pigre, DispatchSourceTimer. Il tracciamento su questi valori è sprecato, o peggio ancora causa aggiornamenti spuri della vista.
@Observable
final class FeedViewModel {
var items: [FeedItem] = []
var isLoading: Bool = false
@ObservationIgnored
private let repository: FeedRepository
@ObservationIgnored
private var refreshTask: Task<Void, Never>?
init(repository: FeedRepository) {
self.repository = repository
}
}
La regola pratica che uso: se un membro non compare mai nel body di alcuna vista, marcalo @ObservationIgnored. Il compilatore non ti obbliga, ma le prestazioni ne beneficiano e l'intento del codice diventa esplicito.
Il caso più insidioso riguarda i tipi annidati. Se la tua classe @Observable contiene un'altra classe (ad esempio un modello di dominio), quella classe deve a sua volta essere @Observable, altrimenti le mutazioni sui suoi campi non propagano. In modo analogo, le mutazioni su collezioni annidate (come items[0].isRead = true) funzionano solo se FeedItem è una classe @Observable. Con una struct devi riassegnare l'elemento (items[0] = updated) perché SwiftUI riveda il vettore. Un bug che ho scoperto solo perché una lista di 200 righe smetteva misteriosamente di aggiornarsi.
Perché @Observable è più veloce di ObservableObject
Il guadagno di prestazioni non deriva dal linguaggio, ma dal modello di invalidazione. Con ObservableObject, il publisher objectWillChange notifica tutte le viste sottoscritte, indipendentemente da quale proprietà è cambiata. SwiftUI confronta quindi il body ricalcolato con quello precedente per capire se qualcosa è effettivamente da ridisegnare. Se hai 30 viste che osservano lo stesso ViewModel, hai 30 rivalutazioni per ogni cambiamento.
Con @Observable, SwiftUI installa un tracker durante il rendering del body. Ogni lettura di una proprietà passa dal getter generato dalla macro, che registra l'accesso nel tracker corrente. SwiftUI conserva quindi una mappa «vista X dipende dalle proprietà [A, B]» e ridisegna solo quando A o B cambiano davvero. È lo stesso principio dei sistemi reattivi fine-grained come SolidJS o dei signal, ma implementato dal compilatore Swift.
Un benchmark che ho fatto su un feed di 500 righe con toggle di stato «letto» mostra riduzioni del 60–80% delle rivalutazioni del body passando a @Observable. In app che usano Liquid Glass e le nuove superfici di iOS 26, dove ogni ridisegno può innescare effetti visivi costosi, la differenza si sente anche in termini di consumo energetico su Apple Silicon.
No. La macro richiede iOS 17, macOS 14, tvOS 17 o watchOS 10 come minimo. Per supportare iOS 16 devi mantenere ObservableObject o incapsulare il ViewModel dietro un #available. Con il calo dell'iOS 16 nelle analytics degli App Store del 2026, la maggior parte dei nuovi progetti può ormai fissare iOS 17 come deployment target.
Devo ancora usare Combine con @Observable?
Combine e Observation risolvono problemi diversi. @Observable sostituisce ObservableObject per il collegamento tra modello e vista, ma se hai pipeline di eventi asincroni (rete, sensori, sync) Combine, oppure AsyncSequence con Swift 6.2, resta valido. Molti team usano AsyncSequence per la logica di dominio e @Observable come layer di presentazione.
Qual è la differenza tra @Bindable e @Binding?
@Bindable crea binding a due vie verso le proprietà di una classe @Observable ricevuta come input; @Binding riceve un binding già costruito verso un valore singolo, tipicamente una struct o un enum. Se vedi @Binding var user: User con User classe, quasi sempre volevi @Bindable var user: User.
Come si abilita @Observable in un progetto Xcode 26?
Non serve alcuna impostazione: la macro è parte della standard library di Swift 5.9+. Assicurati che il deployment target sia iOS 17 o superiore, importa Observation e applica @Observable alla classe. In Swift 6.2, con isolamento MainActor predefinito attivo, la classe erediterà l'isolamento del contesto in cui viene istanziata.
@Observable è compatibile con Swift 6.2 e strict concurrency?
Sì. Con Approachable Concurrency di Swift 6.2 le classi @Observable sono spesso implicitamente @MainActor, coerente con l'uso da SwiftUI. Se ti serve mutarle da un contesto in background, dichiara esplicitamente l'isolamento (ad esempio @MainActor @Observable final class …) e sincronizza gli aggiornamenti con await, mantenendo la mutazione sul main actor.
Guida pratica e completa al framework App Intents in iOS 26: AppEntity con SwiftData, EntityQuery con @Dependency, App Shortcut, integrazione Spotlight semantica e Apple Intelligence in Swift.
Guida pratica all'ereditarietà dei modelli in SwiftData per iOS 26: gerarchie di classi, query polimorfiche, ottimizzazione fetch e migrazione schema con esempi di codice pronti all'uso.