SwiftUI @Observable makró útmutató: view-update graph, @Bindable és Swift 6 (iOS 26)

Nézd meg az @Observable makró útmutatót SwiftUI-hoz: view-update graph, @Bindable, Swift 6 @MainActor izoláció iOS 26 alatt, valós projektpéldákkal és migrációs recepttel.

SwiftUI @Observable makró útmutató (iOS 26)
On this page
  1. Mit csinál pontosan az @Observable makró?
  2. Hogyan alakítja át az @Observable a view-update graphot?
  3. @Observable vs ObservableObject: teljesítmény és API összehasonlítás
  4. Migráció ObservableObject-ről @Observable-re lépésről lépésre
  5. @Bindable property wrapper: mikor és hogyan használjuk
  6. @Observable és Swift 6 strict concurrency
  7. Gyakori buktatók és teljesítmény footgunok
  8. Gyakran ismételt kérdések

Frissítve: 2026. szeptember 2.

Az @Observable makró az Apple Observation frameworkjének beviteli pontja: fordítási időben egy sima Swift osztályból property-szintű változásfigyeléssel rendelkező, Observable-konform típust generál, és a SwiftUI runtime csak azokat a nézeteket rajzolja újra, amelyek a body-ban ténylegesen olvasták a megváltozott tulajdonságot. iOS 17 óta érhető el, iOS 26 és Swift 6.2 alatt pedig @MainActor izolációval és a @Bindable property wrapperrel ez lett a hivatalosan ajánlott state management minta minden új SwiftUI projekthez.

Mit csinál pontosan az @Observable makró?

Az @Observable egy attached member and extension macro, ami a fordítás során három dolgot ad hozzá az osztályodhoz: (1) Observable és Observation.Observable protokoll konformanciát, (2) egy privát ObservationRegistrar példányt, és (3) minden tárolt property köré egy @ObservationTracked wrappert, ami a getter/setter útján registrar.access(...) és registrar.withMutation(...) hívásokkal jelzi a runtime-nak, hogy egy adott property épp olvasva vagy módosítva van.

Ha az Xcode-ban jobb egérgombbal az @Observable-re kattintasz és kiválasztod az Expand Macro menüpontot, pontosan látod, mit ír be a helyedbe a fordító. Ez a boilerplate (a @Published tucatnyi property mögötti üresjárati kódja) most már generált, nem kézzel karbantartott.

import Observation

@Observable
final class OrderTicketModel {
    var symbol: String = "AAPL"
    var quantity: Int = 0
    var limitPrice: Decimal? = nil

    // Ez a property nem hajt UI-t, kizárjuk a követésből:
    @ObservationIgnored
    private var lastValidationTimestamp: Date?
}

Őszintén, a Robinhood Crypto csapatában, ahol az order-entry flow SwiftUI-ra írását vezettem, pont ezért kritikus a @ObservationIgnored: a naplózás, timestamp cache-elés, retry state, ezek gyakran változnak, de sosem hajtanak nézetet. Ha kihagyod az @ObservationIgnored-ot, a runtime feleslegesen jegyzi be őket a függőségi listába, és nagyobb lesz a nyomás a registrarban minden egyes olvasáskor.

Hogyan alakítja át az @Observable a view-update graphot?

A klasszikus ObservableObject modellben a nézet függősége az egész objektum. Ha bármelyik @Published tulajdonság változott, a Combine kiadta az objectWillChange-t, és a SwiftUI diff-elte az adott nézet body-ját még akkor is, ha az a body soha nem olvasta azt a mezőt. Egy 20-property-s SettingsModel mellett ez azt jelentette, hogy egy jelölőnégyzet átbillentése egy teljesen független Text-et is újraszámoltatott.

Az Observation framework alatt a diff granularitása property-szintű. Amikor SwiftUI a body-t hívja, azt egy withObservationTracking blokkba csomagolja, ami az adott passban minden registrar.access(keyPath:) hívást összegyűjt. Ez lesz a nézet dependency set-je. A registrar ezután minden ezt követő mutáció esetén megnézi, tartalmazza-e a set az adott key path-ot, és csak akkor invalidál.

struct OrderSummary: View {
    let ticket: OrderTicketModel  // sima property, NEM @ObservedObject

    var body: some View {
        // Ez a body csak a `symbol`-t olvassa, ezért ez lesz az EGYETLEN függőség.
        // A `quantity` vagy `limitPrice` változása NEM triggereli ezt a nézetet.
        Text("Kereskedés: \(ticket.symbol)")
            .font(.headline)
    }
}

Ez azért fontos, mert a nézetfát ezzel lokálisan tudod optimalizálni. Ha a lista minden sora csak egy modell egyetlen property-jét olvassa, akkor egy másik sor változása egyetlen extra body hívást sem okoz. A Robinhood order book listánál, ahol másodpercenként több száz frissítés fut be websocketen, ez a különbség tette lehetővé, hogy 60 FPS-t tartsunk középkategóriás készülékeken is. A régi @Published alapú modellel a fő szál a listasorok újradiff-elésén küszködött.

A fő tudnivaló: a függőségi set dinamikus. Minden body futás új settel indul. Ha egy conditional branch csak néha olvassa a limitPrice-t, az a body a legutóbb megfigyelt állapot alapján lesz feliratkozva, pontosan úgy, ahogy egy React hook-alapú komponensben elvárnád.

@Observable vs ObservableObject: teljesítmény és API összehasonlítás

A két modell nem drop-in helyettesíthető. Az API és a lifecycle is eltér. Az alábbi táblázat a legfontosabb dimenziók mentén foglalja össze a különbségeket, azzal a fókusszal, amit az Apple hivatalos Migrating from the Observable Object protocol to the Observable macro dokumentuma is kiemel.

JellemzőObservableObject@Observable
Property jelölés@Published kötelező minden UI-t hajtó property-hezNincs jelölés, minden tárolt property automatikusan követve
Nézet függőség granularitásaObjektumszintű (bármi változás → minden feliratkozott view újraszámol)Property-szintű (csak az érintett view-k)
Combine függőségIgen (ObjectWillChangePublisher)Nem, saját ObservationRegistrar
Modell birtoklása view-ban@StateObject (egyszer init, autoclosure)@State (init minden view-újraépítéskor)
Binding projektálás@ObservedObject var m; m.$field@Bindable var m; $m.field
Minimum OSiOS 13+iOS 17+ / macOS 14+
Redraw csökkenés (form-heavy)alapérték~20–30% (Apple és közösségi benchmarkok)

A leggyakrabban félreértett sor a modell birtoklása. A @StateObject egy @autoclosure initializert kap, ezért csak egyszer hívódik meg, a view teljes életciklusára megőrizve a példányt. A @State viszont minden view-újraépítéskor újra kiértékeli az initializert, ezért ha nehéz konstruktora van a modellednek, azt lazy-ként vagy factory helperben érdemes intézni. Ez a különbség önmagában bug-ok forrása a migrációk során, ahogy a Swift fórumon is felmerül visszatérően.

Migráció ObservableObject-ről @Observable-re lépésről lépésre

A migráció gyakorlatban négy mechanikus lépés osztályonként, plusz egy körültekintés az integrációs pontokon. Az Xcode 26 tartalmaz egy Convert to Observable refactor akciót, de a nem-triviális modellek esetén érdemes kézzel átfutni, mert a Combine subscription-ök vagy a Published.Publisher-ként kiadott streamek nem konvertálódnak automatikusan.

  1. Töröld az ObservableObject konformanciát, és tedd fel a class fölé az @Observable-t.
  2. Távolítsd el az @Published attribútumokat. Most már minden stored property automatikusan követve van.
  3. A view oldalon cseréld a @StateObject-et @State-re, az @ObservedObject-et sima let-re vagy var-ra (amelyik a szemantikának megfelel), az @EnvironmentObject-et @Environment(MyModel.self)-re.
  4. Bindingoknál ahol korábban $viewModel.field-et használtál, most tegyél @Bindable var viewModel: MyModel-t a view-ba, és úgy tudsz $viewModel.field-et írni.
// ELŐTTE
final class ProfileModel: ObservableObject {
    @Published var displayName: String = ""
    @Published var isDirty: Bool = false
}

struct ProfileForm: View {
    @StateObject private var model = ProfileModel()
    var body: some View {
        TextField("Név", text: $model.displayName)
    }
}

// UTÁNA
@Observable
final class ProfileModel {
    var displayName: String = ""
    var isDirty: Bool = false
}

struct ProfileForm: View {
    @State private var model = ProfileModel()
    var body: some View {
        @Bindable var model = model  // lokális bindable projektálás
        TextField("Név", text: $model.displayName)
    }
}

A gyakorlatban azt javaslom, egyszerre egy osztályt migrálj. Ne próbáld a projektet nagy PR-ben átforgatni. A vegyes állapot teljesen működőképes: a régi ObservableObject-ek együtt élhetnek az újakkal, de egy adott osztályon ne keverd a két megközelítést. A SwiftData útmutatóban is bemutatott @Model makró egyébként belülről ugyanazt az Observation registrart használja, tehát a @Model-osztályaid már ma is granuláris view-frissítést kapnak.

@Bindable property wrapper: mikor és hogyan használjuk

A @Bindable az a mechanizmus, amivel egy @Observable osztály tulajdonságaiból Binding<Value>-ket tudsz projektálni. Két helyen használható: view property-ként (mint a @State vagy @ObservedObject), vagy a body-n belül lokális változóként. Utóbbi a leggyakoribb minta, ha a modellt kívülről kapod meg és mégis bindingot kell adnod egy child view-nak vagy egy TextField-nek.

struct OrderRow: View {
    let ticket: OrderTicketModel  // csak olvasás, sima referencia

    var body: some View {
        // Lokális @Bindable projektálás — nem birtokolja a modellt,
        // csak bindingot ad a body-n belül:
        @Bindable var ticket = ticket
        Stepper("Mennyiség: \(ticket.quantity)", value: $ticket.quantity, in: 0...1000)
    }
}

Egy fontos, kevesebbet dokumentált részlet: a @Bindable-ből származó Binding-ok bizonyos animációs kontextusokban tudnak tearing-et okozni. Például amikor egy withAnimation blokkban azonnal módosítod a property-t, majd ugyanabban a run-loop iterációban a bindinghez tartozó gesture-callback is beleír. Én ezt a hibát épp egy custom slider gesture-nél kaptam el először. A tapasztalatom szerint legbiztosabban úgy oldod meg, hogy a mutációt egy explicit Task { @MainActor in ... }-ba tereled, vagy a binding set-jét egyedi tranzakcióval fejezed be. Így garantált, hogy az animation frame értékei konzisztensek maradnak.

A @Bindable nem birtokolja a modellt: nem indít lifecycle-t, nincs teardown-ja. Ha a szülő megsemmisül, a Bindable-en át kiosztott bindingok is elmennek. Ezzel szemben a @State igen: ő az, aki a példányt életben tartja. Ha összekevered a két szerepet, ismétlődő deallokációt vagy néma nézetfrissítés-hiányt fogsz látni, amit már többen bejelentettek az Xcode 26 alatt is az Apple Developer Forums-on.

@Observable és Swift 6 strict concurrency

Swift 6 alatt a fordító compile-time garantálja a data race mentességet, és ez az @Observable osztályokra is vonatkozik. A legtisztább minta, ha a modellt @MainActor-ra izolálod: így minden mutáció a fő szálon történik, a SwiftUI view frissítéseivel konzisztensen, és nincs szükség manuális Sendable konformanciára a modell tárolt property-jein.

@Observable
@MainActor
final class OrderTicketModel {
    var symbol: String = "AAPL"
    var quantity: Int = 0

    // Nem-izolált háttérmunka egy actor mögött:
    private let quoteService: QuoteService

    init(quoteService: QuoteService) {
        self.quoteService = quoteService
    }

    func refreshQuote() async {
        // A hívás fut az actoron/nonisolated context-ben,
        // az eredményt viszont MainActor-on írjuk vissza:
        let quote = await quoteService.latestQuote(for: symbol)
        // Itt már MainActor-izoláltak vagyunk, mert a metódus is az.
        self.quantity = quote.suggestedSize
    }
}

Ha a modellt egy Task.detached-ban akarod mutálni, a fordító hibát fog dobni, és jól teszi, mert ez pontosan az a race, ami korábban rejtve maradhatott. A Swift 6.2 által bevezetett approachable concurrency módról részletesen írtam a Swift 6.2 Approachable Concurrency útmutatóban. Ott bemutatom, hogyan lehet a legtöbb Sendable zajt lekapcsolni a UI kódban anélkül, hogy elveszítenéd a fordítói ellenőrzést a valóban párhuzamos rétegekben.

Egy fontos, gyakran figyelmen kívül hagyott részlet: az Observation registrar nem Sendable. Ha a modell nem @MainActor, akkor az egyetlen szálbiztos használati minta, ha egy actor mögé rejted teljesen, és a UI oldalára csak snapshot-értékeket adsz át. iOS 26 alatt az új DiscardingTaskGroup-pal ez tisztán megoldható.

Gyakori buktatók és teljesítmény footgunok

A migráció után a leggyakoribb regresszió nem lassulás, hanem a fordított: kevesebb újrarajzolás, mint amennyi kellene, mert egy computed property elfedi a valós függőségeket. Nézd meg ezt a példát:

@Observable
final class Cart {
    var items: [LineItem] = []

    // MINDEN item változása triggerelni fogja azokat a view-kat,
    // amik ezt olvassák — még ha az itemekkel nem is számolnak:
    var total: Decimal {
        items.reduce(0) { $0 + $1.price * Decimal($1.quantity) }
    }
}

A másik gyakori csapda a nested modellek. Ha egy @Observable tulajdonsága egy másik osztály referencia, amit szintén frissíteni akarsz, akkor annak a belső osztálynak is @Observable-nek kell lennie. A régi ObservableObject-nél a nested ObservableObject-ek nem propagálták automatikusan a változásokat sem. Ez az új modellben már működik, feltéve, hogy mindkét szint fel van annotálva.

Végül: a @Observable csak class-okra alkalmazható. Struct értéktípusokat továbbra is @State/@Binding-gel követ a SwiftUI, természetes mutation-tracking-gel. Ha értéktípus szemantikát akarsz property-szintű observation-nel, akkor ehhez wrappelni kell egy @Observable class-ba. Nincs middle-ground megoldás. Ez a korlát ismert és nyitott az Apple felé; a hivatalos SE-0395 Observability proposal is expliciten kiköti, hogy a design osztály-alapú.

A haptika oldalán az iOS 26 SensoryFeedback szintén együttműködik az Observation frameworkkel: a .sensoryFeedback modifier egy @Observable property változásait tudja triggerként figyelni, granuláris frissítéssel.

Gyakran ismételt kérdések

Mikor használjam az @Observable-t az ObservableObject helyett?

Minden új projektben, ahol iOS 17+ a minimum deployment target, az Apple hivatalosan is ezt ajánlja Xcode 26 alatt. Meglévő projektben a granuláris view-frissítés miatt a nagy modellek (settings, űrlapok, listák) migrálása hoz a legnagyobb performance nyereséget.

Miért nem frissül a nézetem @Observable modellel?

A leggyakoribb ok, hogy a body nem olvassa közvetlenül a property-t (pl. egy helper metóduson keresztül fér hozzá, aminek a hívása nem kerül be a dependency setbe). Másik gyakori ok, hogy @ObservationIgnored-ot tettél egy property-re, ami mégis UI-t hajt. Az Xcode Instruments SwiftUI templatje pontosan megmutatja, milyen függőségeket lát az adott view.

Mi a különbség a @Bindable és a @Binding között?

A @Binding egyetlen értéket forward-ol (két irányban) egy szülő nézetből egy gyerekbe. A @Bindable viszont egy egész @Observable osztály referenciájából projektál bindingokat a $modell.tulajdonsag szintaxissal, ezzel váltjuk le a régi @ObservedObject-tel elérhető $model.field mintát.

Támogatja az @Observable a struct-okat?

Nem, csak osztályokra alkalmazható. A struct értéktípusok mutation-jét a SwiftUI amúgy is természetesen követi @State-en keresztül. Ha értéktípus szemantika mellett property-szintű observation kell, akkor egy @Observable class-ba kell burkolnod az adatot.

Használjak @State-et vagy @StateObject-et egy @Observable modellhez?

Kifejezetten @State-et. A @StateObject csak ObservableObject-tel működik. Fontos különbség: a @State minden view-újraépítéskor kiértékeli az initializert (a példány életciklusa megmarad, de a lezárás lefut), míg a @StateObject autoclosure-t használ és pontosan egyszer inicializál. Ha nehéz konstruktorod van, gondoskodj róla, hogy olcsó legyen újraépíteni.