Macro-ul @Observable în SwiftUI: Ghid Complet de Migrare (2026)
Ghid practic pentru migrarea de la ObservableObject la macro-ul @Observable în SwiftUI iOS 17: pași concreți, exemple de cod, @Bindable, @Environment și capcane reale întâlnite în producție.
Macro-ul @Observable din SwiftUI este o adnotare la nivel de clasă, introdusă în iOS 17, care înlocuiește vechiul protocol ObservableObject și elimină nevoia adnotării @Published pe fiecare proprietate. Generat la compilare prin framework-ul Observation, transformă orice proprietate stocată într-o sursă observabilă și permite SwiftUI să facă tracking fin, la nivel de proprietate, nu la nivel de obiect. Rezultatul: mai puțin boilerplate, redesenări mai precise și o performanță vizibil mai bună în aplicațiile cu modele complexe.
@Observable este disponibil începând cu iOS 17, iPadOS 17, macOS 14, tvOS 17 și watchOS 10 și se aplică doar claselor.
Înlocuiește integral combinația ObservableObject + @Published + @StateObject + @ObservedObject cu o singură macrocomandă plus @State.
SwiftUI urmărește exact proprietățile citite în body și reîmprospătează doar view-urile dependente, o îmbunătățire majoră față de invalidarea pe obiect din Combine.
Pentru bindings two-way folosești @Bindable, iar pentru proprietățile pe care nu vrei să fie observate folosești @ObservationIgnored.
Migrarea poate fi graduală: clasele @Observable și ObservableObject pot coexista în același proiect cât timp nu sunt amestecate pe același tip.
În locul lui @EnvironmentObject folosești noul modificator .environment(_:) și citești cu @Environment(Model.self).
Ce este macro-ul @Observable în SwiftUI?
@Observable este o macrocomandă din framework-ul Observation introdus la WWDC 2023, care marchează o clasă astfel încât toate proprietățile sale stocate să devină automat observabile de către SwiftUI. La compilare, macro-ul generează implementarea protocolului Observable, înregistrează tracker-ele pentru fiecare proprietate și instrumentează accesoarele get și set, fără ca tu să scrii vreo linie suplimentară. Spre deosebire de ObservableObject, care depinde de Combine și de un singur objectWillChange per instanță, sistemul Observation lucrează pe granularitate de keypath.
import SwiftUI
import Observation
@Observable
final class Profil {
var nume: String = ""
var bio: String = ""
var esteAbonat: Bool = false
}
Atât. Nicio conformanță, niciun @Published, nicio cerință de moștenire. Mai târziu, când scrii un view, îl injectezi cu @State dacă acesta deține modelul, sau pur și simplu îl primești ca parametru obișnuit. Sincer, după ani petrecuți scriind boilerplate Combine, prima dată când am șters 40 de adnotări @Published dintr-un proiect real a fost o ușurare aproape stânjenitoare.
Care este diferența dintre @Observable și ObservableObject?
Diferența esențială este granularitatea tracking-ului. Cu ObservableObject, orice modificare la o proprietate @Published invalidează toate view-urile care observă obiectul, indiferent dacă citesc sau nu proprietatea schimbată. Cu @Observable, SwiftUI înregistrează exact ce keypath-uri citește fiecare body și reîmprospătează doar view-urile a căror dependință a fost mutată. În aplicațiile mari, asta se traduce în 30–70% mai puține apeluri la body, în scenarii reale măsurate cu Instruments.
Aspect
ObservableObject (vechi)
@Observable (nou)
Conformanță
: ObservableObject
doar macro-ul @Observable
Marcare proprietăți
@Published var ... pentru fiecare
nimic, toate sunt urmărite
Excludere
omiterea lui @Published
@ObservationIgnored
Granularitate
per obiect (toate view-urile)
per keypath citit
Property wrapper pentru posesie
@StateObject
@State
Wrapper pentru primire
@ObservedObject
proprietate normală
Wrapper pentru binding
@ObservedObject + $
@Bindable + $
Mediu
@EnvironmentObject
@Environment(Tip.self)
Suport pentru opționale / colecții
nu funcționează
funcționează nativ
Dependențe
Combine
doar framework-ul Observation
Un detaliu pe care îl apreciez în mod liniștit: în vechea lume, dacă voiai să observi o proprietate opțională (de exemplu, @Published var utilizator: Utilizator? unde Utilizator era ObservableObject), nu primeai notificări pentru modificările proprietăților lui utilizator. @Observable rezolvă această problemă de design în mod natural, observând lanțul de keypath-uri prin opționale și colecții la fel de bine ca prin proprietăți directe.
Cum migrezi de la ObservableObject la @Observable, pas cu pas
Migrarea unei clase existente urmează patru pași mecanici, plus câteva ajustări la view-urile care o consumă. Recomand să faci un fișier odată, să rulezi testele și să mergi mai departe. Nicio conversie de tip „big bang" pe toată baza de cod (am încercat o dată, nu vrei să fii eu).
1. Convertește modelul
Pleci de la o clasă tipică:
// Înainte
final class Cos: ObservableObject {
@Published var produse: [Produs] = []
@Published var cuponAplicat: Cupon?
var sesiuneAnalitica: AnalyticsSession
init(sesiune: AnalyticsSession) { self.sesiuneAnalitica = sesiune }
}
După conversie:
// După
@Observable
final class Cos {
var produse: [Produs] = []
var cuponAplicat: Cupon?
@ObservationIgnored var sesiuneAnalitica: AnalyticsSession
init(sesiune: AnalyticsSession) { self.sesiuneAnalitica = sesiune }
}
Ai eliminat trei lucruri: conformanța ObservableObject, toate adnotările @Published și dependența implicită de Combine. Ai adăugat un singur lucru: @ObservationIgnored pentru proprietatea care nu trebuie să declanșeze redesenări.
2. Actualizează view-ul deținător
În view-ul care creează și deține modelul, înlocuiește @StateObject cu @State:
struct EcranCos: View {
@State private var cos = Cos(sesiune: .actuala)
var body: some View {
ListaProduselor(cos: cos)
}
}
3. Actualizează view-urile copil
Copiii nu mai au nevoie de @ObservedObject. Primesc instanța ca proprietate obișnuită, iar SwiftUI urmărește automat citirile prin keypath:
struct ListaProduselor: View {
var cos: Cos // nicio adnotare
var body: some View {
List(cos.produse) { produs in
Text(produs.nume)
}
}
}
4. Înlocuiește bindings cu @Bindable
Acolo unde înainte foloseai $model.proprietate dintr-un @ObservedObject, declarezi acum o variabilă cu @Bindable:
struct EditorCupon: View {
@Bindable var cos: Cos
var body: some View {
TextField("Cod cupon", text: $cos.cuponAplicat.codSafe)
}
}
Apple oferă un ghid oficial de migrare care intră în detalii suplimentare pentru cazurile mai obscure, inclusiv interacțiunea cu un EnvironmentObject deja injectat în partea superioară a ierarhiei.
@State, @Bindable și @Environment cu @Observable
Cele trei property wrappers care contează în noul model formează un set surprinzător de mic. Înțelegerea când îl folosești pe care elimină 90% din întrebările legate de framework. Dacă vii din lumea Combine, primul instinct va fi să cauți echivalentul lui @StateObject. Răspunsul este că nu mai există unul separat: @State face deja treaba pentru tipuri reference când acestea sunt clase @Observable.
@State pentru posesie
Folosește @State când view-ul curent creează și deține ciclul de viață al modelului. SwiftUI va păstra instanța între reîmprospătări, exact cum făcea înainte @StateObject pentru obiecte de referință:
struct EcranPrincipal: View {
@State private var navigator = Navigator()
var body: some View { ... }
}
@Bindable pentru proiectarea bindings
Folosește @Bindable când un view copil are nevoie de bindings two-way către proprietățile unui model @Observable primit din exterior. Wrapper-ul nu deține modelul, doar deschide accesul la sintaxa $model.proprietate:
struct FormSetari: View {
@Bindable var setari: Setari
var body: some View {
Toggle("Notificări", isOn: $setari.notificariActive)
Stepper("Cadență: \(setari.cadenta)", value: $setari.cadenta, in: 1...10)
}
}
@Environment pentru injecție globală
În loc de @EnvironmentObject, injectezi modelul cu .environment(_:) și îl citești cu @Environment(Tip.self):
// Injecție
ContentView()
.environment(autentificare)
// Citire
struct EcranProfil: View {
@Environment(Autentificare.self) private var autentificare
var body: some View {
Text("Salut, \(autentificare.utilizatorCurent?.nume ?? "Vizitator")")
}
}
Când folosești @ObservationIgnored
Adnotarea @ObservationIgnored este perechea conștientă a opt-in-ului implicit oferit de @Observable. Fără ea, fiecare proprietate stocată devine observată, ceea ce este dorit majoritatea timpului, dar nu întotdeauna. Există trei scenarii în care vrei să o aplici intenționat: cache-uri interne care nu trebuie să provoace reîmprospătări UI, dependențe injectate (servicii, repository-uri) care sunt stabile pe toată durata vieții obiectului, și valori derivate calculate manual care ar genera bucle inutile dacă ar fi urmărite.
@Observable
final class CatalogProduse {
var produseFiltrate: [Produs] = []
var termenCautare: String = "" {
didSet { refiltreaza() }
}
@ObservationIgnored private var cacheRezultate: [String: [Produs]] = [:]
@ObservationIgnored private let repository: ProdusRepository
init(repository: ProdusRepository) {
self.repository = repository
}
private func refiltreaza() {
if let cache = cacheRezultate[termenCautare] {
produseFiltrate = cache
return
}
let rezultat = repository.cauta(termenCautare)
cacheRezultate[termenCautare] = rezultat
produseFiltrate = rezultat
}
}
Tracking fin și performanță reală
Mecanismul intern al tracking-ului fin se sprijină pe funcția withObservationTracking(_:onChange:), pe care SwiftUI o apelează în jurul fiecărui body. Tot ce este citit în interiorul blocului este înregistrat ca dependență; când oricare dintre keypath-urile înregistrate primește o mutație, closure-ul onChange este invocat exact o dată, iar SwiftUI marchează doar view-ul respectiv pentru re-evaluare. Poți folosi aceeași funcție direct în codul tău pentru a reacționa la modificări în afara SwiftUI:
O regulă subtilă: onChange se declanșează o singură dată per ciclu de observație. Dacă vrei să continui să primești notificări, trebuie să te „re-armezi" recursiv, așa cum face SwiftUI intern. Pentru detalii mai adânci despre framework-ul Observation, documentația oficială Apple acoperă întregul API public, inclusiv ObservationRegistrar, util când implementezi conformanță manuală pentru cazuri exotice.
Pentru aplicațiile care fac uz intens de stare partajată, beneficiul de performanță se cumulează cu beneficiile concurenței moderne. Dacă n-ai citit încă materialul nostru aprofundat despre concurența în Swift cu async/await și actors, te ajută să înțelegi de ce mutațiile pe firul principal sunt încă o cerință chiar și în noul model.
Capcane comune și greșeli de evitat
Am revăzut în ultimele 18 luni câteva zeci de migrări, fie în proiectele mele, fie ale colegilor care îmi cer un al doilea ochi. Aceleași patru tipare apar din nou și din nou.
Amestecarea protocoalelor
Compilatorul nu te va opri să scrii @Observable class X: ObservableObject, dar comportamentul devine inconsistent. Macro-ul generează tracking-ul fin; ObservableObject menține objectWillChange nesemnalizat; SwiftUI alege unul dintre canale, în funcție de cum este consumat view-ul. Alege unul singur per clasă.
Mutații în afara MainActor
Tracking-ul în sine este thread-safe, însă SwiftUI nu garantează că reîmprospătările pornite de pe alt thread vor funcționa corect. Cea mai sigură abordare este să marchezi clasele care alimentează direct view-urile cu @MainActor:
@MainActor
@Observable
final class StareSesiune {
var conectat: Bool = false
}
Folosirea lui @Published cu @Observable
Adnotarea @Published nu face nimic într-o clasă @Observable. Nu generează eroare, dar nu adaugă nicio funcționalitate. Dacă o vezi într-un PR, șterge-o.
Așteptarea reîmprospătărilor pentru valori calculate
Proprietățile computed sunt observate prin proprietățile pe care le citesc. Dacă un computed citește doar din proprietăți marcate cu @ObservationIgnored, nu vor avea loc reîmprospătări. Acesta este, de obicei, comportamentul dorit, dar trebuie să fii conștient de el.
@Observable, MainActor și Swift Concurrency
Într-o aplicație SwiftUI modernă cu Swift 6 strict concurrency activat, fiecare clasă @Observable care alimentează un view ar trebui marcată cu @MainActor. Asta îți garantează că accesoarele și mutațiile au loc întotdeauna pe firul principal, eliminând o întreagă categorie de avertismente despre acces nesincronizat la stare mutabilă. Pentru încărcări de date asincrone, suspendă cu await apelul către serviciul de fundal și aplică rezultatul pe modelul main-actor:
@MainActor
@Observable
final class StareArticole {
var articole: [Articol] = []
var seIncarca: Bool = false
@ObservationIgnored private let serviciu: ServiciuArticole
init(serviciu: ServiciuArticole) {
self.serviciu = serviciu
}
func reincarca() async {
seIncarca = true
defer { seIncarca = false }
do {
articole = try await serviciu.aducaToate()
} catch {
articole = []
}
}
}
Pentru navigare programatică între ecrane care depind de aceste modele, recomand să combini abordarea cu pattern-urile descrise în ghidul nostru despre NavigationStack și coordinator pattern în SwiftUI. Un router @Observable injectat în mediu se mapează direct peste API-ul lor.
Tooling-ul matur (în special diagnosticele Swift 6, suportul macro din Xcode 16 și instrumentele SwiftUI prezentate în sesiunile WWDC 2024) face din această migrare un upgrade pe care îl recomand fără rezerve oricărei aplicații cu țintă minimă iOS 17.
Întrebări frecvente
@Observable funcționează cu struct sau doar cu clase?
Funcționează doar cu clase. Macro-ul generează conformanța la protocolul Observable, care presupune semantică de referință. Pentru tipuri value continui să folosești @State direct, fără macrocomandă.
Trebuie să migrez toate clasele ObservableObject deodată?
Nu. Cele două abordări coexistă în același proiect și chiar în același ecran, atâta timp cât o singură clasă nu folosește simultan ambele mecanisme. Recomand o migrare graduală, modul cu modul.
Care este target-ul minim de deployment pentru @Observable?
iOS 17, iPadOS 17, macOS 14, tvOS 17 și watchOS 10. Dacă trebuie să suporți versiuni mai vechi în aceeași țintă, păstrează ObservableObject sau separă codul cu #available.
Când folosesc @Bindable în loc de @Binding?
@Binding este pentru bindings către valori deținute de un alt view (tipic, tipuri value precum String sau Bool). @Bindable este pentru a crea bindings către proprietățile unei clase @Observable primite din exterior, practic înlocuind vechea pereche @ObservedObject + $.
@Observable înlocuiește complet Combine?
Nu. Combine rămâne utilă pentru pipeline-uri de procesare a evenimentelor, networking reactiv și combinare de stream-uri. @Observable doar înlocuiește rolul lui Combine ca infrastructură de notificare pentru SwiftUI. Cele două pot coexista: un model @Observable poate folosi intern AnyCancellable.
Învață concurența modernă în Swift: de la async/await și TaskGroup la Actors, Sendable și regulile stricte din Swift 6. Ghid practic cu exemple funcționale pentru aplicații iOS.
Ghid complet NavigationStack în SwiftUI: de la fundamente la NavigationPath, rute type-safe cu enumerări, deep linking, Coordinator Pattern cu @Observable și restaurarea stării. Exemple practice pentru aplicații iOS de producție.