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-hez | Nincs jelölés, minden tárolt property automatikusan követve |
| Nézet függőség granularitása | Objektumszintű (bármi változás → minden feliratkozott view újraszámol) | Property-szintű (csak az érintett view-k) |
| Combine függőség | Igen (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 OS | iOS 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.
- Töröld az
ObservableObjectkonformanciát, és tedd fel a class fölé az@Observable-t. - Távolítsd el az
@Publishedattribútumokat. Most már minden stored property automatikusan követve van. - A view oldalon cseréld a
@StateObject-et@State-re, az@ObservedObject-et simalet-re vagyvar-ra (amelyik a szemantikának megfelel), az@EnvironmentObject-et@Environment(MyModel.self)-re. - 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.




