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)

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.

  • Az @Observable makró leváltja az ObservableObject + @Published párost, és kizárólag osztályokra alkalmazható (iOS 17+, Swift 5.9+).
  • A SwiftUI a withObservationTracking köré épített invalidációval property-enként követi a body olvasásait, tehát csak az érintett nézetek frissülnek.
  • Form- és lista-nehéz képernyőkön mérhetően 20–30%-kal kevesebb újrarajzolás történik, mint a régi @Published modellel.
  • A bindingot már nem @ObservedObject-ből projektáljuk, hanem @Bindable-lel, és a modell birtoklása @State-en keresztül történik, nem @StateObject-en.
  • A számított tulajdonságok minden mögöttük olvasott tárolt property-t hozzáadnak a nézet függőségeihez. Ez a leggyakoribb rejtett teljesítmény regresszió a migrációk után.
  • Swift 6 strict concurrency alatt a @MainActor izoláció a modellen a fordítóval ellenőrzött szálbiztonságot ad; a nem-UI állapotot érdemes @ObservationIgnored-dal kizárni.

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.

A Szerzőről Mei-Lin Chen

Mei-Lin joined Robinhood in 2020 as an iOS engineer on the Crypto team and stayed through the SwiftUI rewrite of the order-entry flow before leaving in 2025. She also did a two-year stint at Asana earlier in her career working on the iPad app and the Mac Catalyst port. She writes about the parts of Apple's frameworks that the WWDC talks gloss over - what Observable actually does to your view-update graph, why @Bindable bindings tear in some animation contexts, and the surprisingly deep rabbit hole of Swift macros for boilerplate elimination. She has shipped two indie apps to the App Store, one of which hit #4 in the Health & Fitness category for a week in 2023. Mei-Lin is based in Seattle and has been writing Swift for 8 years.