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.

@Observable SwiftUI: Ghid Migrare (2026)

Actualizat: 27 mai 2026

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.

AspectObservableObject (vechi)@Observable (nou)
Conformanță: ObservableObjectdoar macro-ul @Observable
Marcare proprietăți@Published var ... pentru fiecarenimic, toate sunt urmărite
Excludereomiterea lui @Published@ObservationIgnored
Granularitateper obiect (toate view-urile)per keypath citit
Property wrapper pentru posesie@StateObject@State
Wrapper pentru primire@ObservedObjectproprietate normală
Wrapper pentru binding@ObservedObject + $@Bindable + $
Mediu@EnvironmentObject@Environment(Tip.self)
Suport pentru opționale / colecțiinu funcționeazăfuncționează nativ
DependențeCombinedoar 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:

import Observation

func urmareste(profil: Profil) {
    withObservationTracking {
        print("Nume curent: \(profil.nume)")
    } onChange: {
        print("Numele s-a schimbat, re-execut")
        urmareste(profil: profil)   // re-armare manuală
    }
}

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.

Lukas Müller
Despre Autor Lukas Müller

iOS developer and Swift author since the Objective-C days. Spends his evenings on side projects and his mornings on SwiftUI internals.