Bevezetés: Miért változott meg minden az állapotkezelésben?
Ha már egy ideje fejlesztesz SwiftUI-ban, valószínűleg emlékszel azokra az időkre, amikor az ObservableObject, a @Published, a @StateObject, az @ObservedObject és az @EnvironmentObject property wrapperek között kellett navigálnod. Őszintén? Nem volt mindig egyértelmű, melyiket mikor kell használni – és a rossz választás rejtélyes hibákat vagy felesleges újrarajzolásokat eredményezett.
Na de aztán jött az iOS 17.
Az Apple a WWDC 2023-on bemutatta az Observation keretrendszert és az @Observable makrót, ami gyökeresen megváltoztatta a SwiftUI állapotkezelési modelljét. 2025-re és 2026-ra ez a megközelítés érett, stabil, és az Apple által hivatalosan ajánlott módszer lett az új projektek számára. A régi ObservableObject protokoll ugyan továbbra is működik, de az Apple aktívan ösztönzi a migrációt az új rendszerre.
Ebben az útmutatóban mindent megtanulsz, amit az Observation keretrendszer használatáról tudni kell. Végigmegyünk az @Observable, @State, @Bindable és @Environment property wrappereken, gyakorlati példákon keresztül bemutatjuk a helyes használatukat, és megnézzük, hogyan migrálhatjuk a meglévő kódunkat. Szóval, vágjunk bele!
Az Observation keretrendszer alapjai
Az Observation keretrendszer a Swift 5.9-ben bevezetett makrórendszerre épül. Az @Observable makró automatikusan átalakítja az osztályodat úgy, hogy a SwiftUI képes legyen nyomon követni, mely tulajdonságokat olvassa egy nézet, és csak akkor frissíti azt, amikor a ténylegesen használt tulajdonság változik.
Ez hatalmas teljesítménybeli különbség a régi ObservableObject-hoz képest. Korábban bármely @Published tulajdonság változása kiváltotta az összes figyelő nézet újrarajzolását – függetlenül attól, hogy a nézet tényleg használta-e azt a tulajdonságot. Kicsit olyan volt, mintha az egész házban felkapcsolnád a villanyt, csak mert a konyhában kell a fény.
Hogyan működik a háttérben?
Amikor az @Observable makrót alkalmazod egy osztályra, a fordító a következőket teszi:
- Minden tárolt tulajdonságot számított tulajdonsággá alakít
- A getter-ben regisztrálja, hogy a tulajdonságot olvasták (access tracking)
- A setter-ben értesíti a megfigyelőket a változásról (change notification)
- A tényleges értékeket egy belső tárolóban helyezi el
Ez a mechanizmus lehetővé teszi a finom szemcsézettségű megfigyelést (fine-grained observation). Gyakorlatilag a SwiftUI pontosan tudja, melyik nézet melyik tulajdonságot olvasta, és kizárólag a szükséges nézeteket frissíti. Elég elegáns megoldás.
import Observation
@Observable
class UserProfile {
var name: String = ""
var email: String = ""
var avatarURL: URL?
var bio: String = ""
// A számított tulajdonságok is megfigyelhetők!
var displayName: String {
name.isEmpty ? "Névtelen felhasználó" : name
}
}
Ez az egyszerű osztály az Observation keretrendszer erejével automatikusan nyomon követi az összes tárolt tulajdonságát. Ha egy nézet csak a name tulajdonságot olvassa, akkor csak a name változásakor fog újrarajzolódni – az email, avatarURL vagy bio változása nem hat rá. Pont így kéne működnie az egésznek, nem?
Az @State property wrapper: Helyi állapot kezelése
Az @State property wrapper az Observation keretrendszerrel együtt használva kettős szerepet tölt be. Egyszerű értéktípusoknál (String, Int, Bool, struct) ugyanúgy működik, mint korábban – helyi, a nézet által birtokolt állapotot hoz létre.
De – és ez a fontos rész – referencia típusoknál (@Observable osztályoknál) az @State veszi át a korábbi @StateObject szerepét!
@State egyszerű típusokkal
struct CounterView: View {
@State private var count = 0
@State private var label = "Számláló"
var body: some View {
VStack(spacing: 16) {
Text(label)
.font(.headline)
Text("\(count)")
.font(.largeTitle)
.fontWeight(.bold)
HStack(spacing: 20) {
Button("Csökkentés") {
count -= 1
}
Button("Növelés") {
count += 1
}
}
}
.padding()
}
}
@State @Observable osztállyal
A modern SwiftUI-ban az @State property wrappert használjuk @Observable osztályok tartására is, amikor a nézet birtokolja az adott objektumot. Lényegében ez a korábbi @StateObject helyettesítője – csak éppen egyszerűbb.
@Observable
class TodoListViewModel {
var todos: [TodoItem] = []
var newTodoTitle: String = ""
var filter: TodoFilter = .all
var filteredTodos: [TodoItem] {
switch filter {
case .all: return todos
case .active: return todos.filter { !$0.isCompleted }
case .completed: return todos.filter { $0.isCompleted }
}
}
var activeTodoCount: Int {
todos.filter { !$0.isCompleted }.count
}
func addTodo() {
guard !newTodoTitle.trimmingCharacters(in: .whitespaces).isEmpty else { return }
let todo = TodoItem(title: newTodoTitle)
todos.append(todo)
newTodoTitle = ""
}
func toggleTodo(_ todo: TodoItem) {
if let index = todos.firstIndex(where: { $0.id == todo.id }) {
todos[index].isCompleted.toggle()
}
}
func deleteTodo(_ todo: TodoItem) {
todos.removeAll { $0.id == todo.id }
}
}
struct TodoItem: Identifiable {
let id = UUID()
var title: String
var isCompleted: Bool = false
}
enum TodoFilter: String, CaseIterable {
case all = "Mind"
case active = "Aktív"
case completed = "Kész"
}
struct TodoListView: View {
// Az @State tartja életben a ViewModel-t
// a nézet újrarajzolásakor is!
@State private var viewModel = TodoListViewModel()
var body: some View {
NavigationStack {
VStack {
// Szűrő gombok
Picker("Szűrő", selection: $viewModel.filter) {
ForEach(TodoFilter.allCases, id: \.self) { filter in
Text(filter.rawValue).tag(filter)
}
}
.pickerStyle(.segmented)
.padding(.horizontal)
// Teendők listája
List {
ForEach(viewModel.filteredTodos) { todo in
TodoRowView(todo: todo) {
viewModel.toggleTodo(todo)
}
}
.onDelete { indexSet in
for index in indexSet {
viewModel.deleteTodo(viewModel.filteredTodos[index])
}
}
}
// Új teendő hozzáadása
HStack {
TextField("Új teendő...", text: $viewModel.newTodoTitle)
.textFieldStyle(.roundedBorder)
Button("Hozzáadás") {
viewModel.addTodo()
}
.disabled(viewModel.newTodoTitle.isEmpty)
}
.padding()
}
.navigationTitle("Teendők (\(viewModel.activeTodoCount))")
}
}
}
Figyeld meg, hogy a $viewModel.filter és $viewModel.newTodoTitle szintaxis automatikusan működik az @State-tel tartott @Observable objektumoknál. Nem kell semmi extra – és ez nagyon jó érzés, ha a régi rendszerből jössz.
A @Bindable property wrapper: Kétirányú kötés külső objektumokhoz
A @Bindable egy új property wrapper, amelyet az Observation keretrendszerrel együtt vezettek be. Akkor kell használnod, amikor egy @Observable objektumot kívülről kapsz (nem a nézet hozza létre), és kétirányú kötést szeretnél a tulajdonságaihoz.
Mikor kell @Bindable-t használni?
A kulcskérdés egyszerű: Ki birtokolja az objektumot?
- Ha a nézet saját maga hozza létre az objektumot → használj
@State-et - Ha a nézet kívülről kapja és kötést kell létrehozni → használj
@Bindable-t - Ha a nézet kívülről kapja és csak olvassa → nem kell semmilyen wrapper
Ezt a hármast érdemes fejben tartani, mert a legtöbb döntés erre vezethető vissza.
@Observable
class UserSettings {
var username: String = ""
var notificationsEnabled: Bool = true
var theme: AppTheme = .system
var fontSize: Double = 16.0
}
enum AppTheme: String, CaseIterable {
case light = "Világos"
case dark = "Sötét"
case system = "Rendszer"
}
// A szülő nézet birtokolja a settings objektumot
struct SettingsContainerView: View {
@State private var settings = UserSettings()
var body: some View {
NavigationStack {
// A settings objektumot átadjuk a gyermek nézetnek
SettingsFormView(settings: settings)
.navigationTitle("Beállítások")
}
}
}
// A gyermek nézet @Bindable-lel veszi át,
// mert kétirányú kötést kell létrehoznia
struct SettingsFormView: View {
@Bindable var settings: UserSettings
var body: some View {
Form {
Section("Profil") {
TextField("Felhasználónév", text: $settings.username)
}
Section("Megjelenés") {
Picker("Téma", selection: $settings.theme) {
ForEach(AppTheme.allCases, id: \.self) { theme in
Text(theme.rawValue).tag(theme)
}
}
VStack(alignment: .leading) {
Text("Betűméret: \(Int(settings.fontSize)) pt")
Slider(value: $settings.fontSize, in: 12...24, step: 1)
}
}
Section("Értesítések") {
Toggle("Értesítések engedélyezése",
isOn: $settings.notificationsEnabled)
}
}
}
}
// Ha egy nézet csak olvassa az objektumot,
// nem kell semmilyen wrapper
struct SettingsPreviewView: View {
var settings: UserSettings // Nincs wrapper!
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Felhasználó: \(settings.username)")
Text("Téma: \(settings.theme.rawValue)")
Text("Betűméret: \(Int(settings.fontSize)) pt")
}
.padding()
}
}
@Bindable vs @Binding: Mi a különbség?
Sokan összekeverik a @Bindable és a @Binding property wrappereket, és bevallom, az elnevezés nem sokat segít. Íme a lényeges különbség:
@Binding– Egyetlen értékhez ad kétirányú kötést. Például:@Binding var isOn: Bool@Bindable– Egy @Observable objektumhoz ad hozzáférést, amelyből kötéseket hozhatsz létre a$szintaxissal
Másképp fogalmazva: a @Binding egy konkrét értékre mutat, míg a @Bindable egy objektumot csomagol be, amelynek bármelyik tulajdonságához létrehozhatsz kötést. Ha egyszer leesik a tantusz, utána már természetes lesz.
Az @Environment használata: Alkalmazásszintű állapot megosztása
Az @Environment property wrapper az Observation keretrendszer mellett is megmaradt, de a használata jóval egyszerűbbé vált. Korábban az @EnvironmentObject-et kellett használnod ObservableObject-ekhez – most viszont az @Environment közvetlenül működik @Observable osztályokkal.
Ez az egyik kedvenc változásom az egészben.
Az @Observable objektum beillesztése a környezetbe
@Observable
class AppState {
var currentUser: User?
var isLoggedIn: Bool { currentUser != nil }
var selectedTab: Tab = .home
var unreadNotificationCount: Int = 0
func login(user: User) {
currentUser = user
}
func logout() {
currentUser = nil
selectedTab = .home
}
}
struct User: Identifiable {
let id: UUID
var name: String
var email: String
}
enum Tab: String {
case home = "Főoldal"
case search = "Keresés"
case profile = "Profil"
}
// Az alkalmazás belépési pontján adjuk hozzá a környezethez
@main
struct MyApp: App {
@State private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environment(appState)
}
}
}
// Bármelyik gyermek nézet hozzáférhet az @Environment segítségével
struct ContentView: View {
@Environment(AppState.self) private var appState
var body: some View {
// Itt @Bindable-t használunk, mert kötésre van szükségünk
@Bindable var appState = appState
TabView(selection: $appState.selectedTab) {
HomeView()
.tabItem {
Label(Tab.home.rawValue, systemImage: "house")
}
.tag(Tab.home)
SearchView()
.tabItem {
Label(Tab.search.rawValue, systemImage: "magnifyingglass")
}
.tag(Tab.search)
ProfileView()
.tabItem {
Label(Tab.profile.rawValue, systemImage: "person")
}
.badge(appState.unreadNotificationCount)
.tag(Tab.profile)
}
}
}
struct ProfileView: View {
@Environment(AppState.self) private var appState
var body: some View {
VStack(spacing: 20) {
if let user = appState.currentUser {
Text("Üdv, \(user.name)!")
.font(.title)
Text(user.email)
.foregroundStyle(.secondary)
Button("Kijelentkezés", role: .destructive) {
appState.logout()
}
} else {
Text("Nincs bejelentkezve")
Button("Bejelentkezés") {
let user = User(id: UUID(), name: "Kiss Péter", email: "[email protected]")
appState.login(user: user)
}
}
}
.padding()
}
}
Az @Environment és @Bindable együttes használata
Van egy fontos mintázat, amit érdemes megjegyezned: ha az @Environment-ből kapott objektumhoz kötést szeretnél létrehozni, a nézet body-ján belül hozz létre egy lokális @Bindable változót. Ez a SwiftUI hivatalosan ajánlott mintája, és elsőre kicsit furcsának tűnhet, de működik:
struct EditProfileView: View {
@Environment(AppState.self) private var appState
var body: some View {
// Lokális @Bindable a body-n belül
@Bindable var appState = appState
if var user = appState.currentUser {
Form {
TextField("Név", text: Binding(
get: { user.name },
set: { newValue in
user.name = newValue
appState.currentUser = user
}
))
}
}
}
}
Migráció az ObservableObject-ről az @Observable-re
Ha meglévő projekted van, amely az ObservableObject protokollt használja, érdemes fokozatosan átállni az új rendszerre. A jó hír: a migráció általában meglepően egyszerű. De azért vannak buktatók.
Lépésről lépésre migráció
Az Apple hivatalos ajánlása szerint a következő cseréket kell elvégezned:
ObservableObject→@Observablemakró@Published→ Töröld (nem kell semmi helyette!)@StateObject→@State@ObservedObject(ha kötésre van szükség) →@Bindable@ObservedObject(ha csak olvasás) → Semmilyen wrapper@EnvironmentObject→@Environment(TypusNev.self).environmentObject(obj)→.environment(obj)
Tulajdonképpen a lista rövidebb, mint gondolnád. A legtöbb esetben inkább törlünk dolgokat, mint hozzáadunk.
Gyakorlati migráció: Előtte és utána
// ========== RÉGI KÓD (ObservableObject) ==========
class ShoppingCartOld: ObservableObject {
@Published var items: [CartItem] = []
@Published var promoCode: String = ""
@Published var isLoading: Bool = false
var totalPrice: Double {
items.reduce(0) { $0 + $1.price * Double($1.quantity) }
}
var discountedPrice: Double {
if promoCode == "KEDVEZMENY20" {
return totalPrice * 0.8
}
return totalPrice
}
func addItem(_ item: CartItem) {
items.append(item)
}
func checkout() async {
isLoading = true
defer { isLoading = false }
// Fizetési logika...
}
}
// Régi nézetek:
struct CartViewOld: View {
@StateObject private var cart = ShoppingCartOld()
var body: some View {
CartContentOld(cart: cart)
.environmentObject(cart)
}
}
struct CartContentOld: View {
@ObservedObject var cart: ShoppingCartOld
var body: some View {
// ...
TextField("Promó kód", text: $cart.promoCode)
}
}
struct CartBadgeOld: View {
@EnvironmentObject var cart: ShoppingCartOld
var body: some View {
Text("\(cart.items.count)")
}
}
// ========== ÚJ KÓD (@Observable) ==========
@Observable
class ShoppingCart {
var items: [CartItem] = [] // Nincs @Published!
var promoCode: String = ""
var isLoading: Bool = false
var totalPrice: Double {
items.reduce(0) { $0 + $1.price * Double($1.quantity) }
}
var discountedPrice: Double {
if promoCode == "KEDVEZMENY20" {
return totalPrice * 0.8
}
return totalPrice
}
func addItem(_ item: CartItem) {
items.append(item)
}
func checkout() async {
isLoading = true
defer { isLoading = false }
// Fizetési logika...
}
}
struct CartItem: Identifiable {
let id = UUID()
var name: String
var price: Double
var quantity: Int
}
// Új nézetek:
struct CartView: View {
@State private var cart = ShoppingCart() // @StateObject → @State
var body: some View {
CartContent(cart: cart)
.environment(cart) // .environmentObject → .environment
}
}
struct CartContent: View {
@Bindable var cart: ShoppingCart // @ObservedObject → @Bindable
var body: some View {
VStack {
List(cart.items) { item in
HStack {
Text(item.name)
Spacer()
Text("\(item.price, specifier: "%.0f") Ft × \(item.quantity)")
}
}
TextField("Promó kód", text: $cart.promoCode)
.textFieldStyle(.roundedBorder)
.padding()
Text("Összesen: \(cart.discountedPrice, specifier: "%.0f") Ft")
.font(.headline)
}
}
}
struct CartBadge: View {
@Environment(ShoppingCart.self) private var cart // @EnvironmentObject → @Environment
var body: some View {
Text("\(cart.items.count)")
}
}
Látod a különbséget? Az új kód nemcsak rövidebb, de logikusabb is. Kevesebb property wrapper, kevesebb döntés, kevesebb lehetőség a hibázásra.
Teljesítmény optimalizálás az Observation keretrendszerrel
Az Observation keretrendszer egyik legnagyobb előnye a teljesítmény javulás. De azért érdemes tudni, hogyan hozhatod ki belőle a legtöbbet.
A finom szemcsézettségű frissítések ereje
A régi ObservableObject-tel minden @Published változás kiváltotta az objectWillChange értesítést, ami az összes figyelő nézetet frissítette. Az @Observable keretrendszer ezzel szemben csak azokat a nézeteket frissíti, amelyek ténylegesen olvassák a megváltozott tulajdonságot.
Lássuk ezt egy konkrét példán:
@Observable
class DashboardData {
var salesCount: Int = 0
var revenue: Double = 0.0
var newCustomers: Int = 0
var serverStatus: String = "Online"
var lastUpdated: Date = .now
}
// Ez a nézet CSAK AKKOR frissül,
// ha a salesCount vagy a revenue változik!
struct SalesWidget: View {
var data: DashboardData
var body: some View {
VStack {
Text("Eladások: \(data.salesCount)")
Text("Bevétel: \(data.revenue, specifier: "%.0f") Ft")
}
}
}
// Ez a nézet CSAK AKKOR frissül,
// ha a serverStatus változik!
struct StatusWidget: View {
var data: DashboardData
var body: some View {
Label(data.serverStatus, systemImage: "server.rack")
}
}
Ebben a példában, ha a salesCount változik, kizárólag a SalesWidget fog újrarajzolódni – a StatusWidget érintetlen marad. A régi rendszerben mindkettő frissült volna. Nagyobb alkalmazásoknál ez óriási különbséget jelent.
Tulajdonságok elrejtése a megfigyelés elől
Néha vannak olyan tulajdonságok, amelyeknek a változása nem kell, hogy frissítést váltson ki. Tipikus példa erre egy belső cache vagy egy gyorsan változó pozíció érték. Az @ObservationIgnored makróval jelölheted meg ezeket:
@Observable
class MediaPlayer {
var currentTrack: String = ""
var isPlaying: Bool = false
var volume: Double = 0.5
// Ez a tulajdonság nem vált ki nézetfrissítést
@ObservationIgnored
var internalPlaybackPosition: TimeInterval = 0
// A cache sem vált ki frissítést
@ObservationIgnored
var imageCache: [String: UIImage] = [:]
}
Haladó mintázatok és tippek
Az Observation keretrendszer használata SwiftUI-n kívül
Ami igazán tetszik az Observation keretrendszerben, az az, hogy nem csak SwiftUI-ban használható. A withObservationTracking függvénnyel bármilyen Swift kódban nyomon követheted a változásokat:
@Observable
class NetworkMonitor {
var isConnected: Bool = true
var connectionType: String = "WiFi"
}
// Használat UIKit-ben vagy sima Swift kódban
func startMonitoring(monitor: NetworkMonitor) {
withObservationTracking {
// A "apply" closure-ben hozzáférünk a megfigyelt tulajdonságokhoz
print("Kapcsolat állapota: \(monitor.isConnected)")
print("Kapcsolat típusa: \(monitor.connectionType)")
} onChange: {
// Ez a closure egyszer hívódik meg,
// amikor bármelyik megfigyelt tulajdonság változik
print("A hálózati állapot megváltozott!")
// Fontos: a withObservationTracking csak EGYSZER értesít!
// Ha folyamatos megfigyelésre van szükséged,
// újra kell hívnod:
DispatchQueue.main.async {
startMonitoring(monitor: monitor)
}
}
}
Egy fontos buktató: A withObservationTracking onChange closure-je csak egyszer hívódik meg, az első változáskor. Ha folyamatos megfigyelésre van szükséged, rekurzívan kell meghívnod a függvényt. Erre tényleg figyelj, mert könnyen elfelejthető!
@Observable és SwiftData együtt
Ha SwiftData-t használsz, van egy jó hírem: a @Model makró automatikusan @Observable-ként is megjelöli a modelleket, szóval nincs szükség extra konfigurációra. Egyszerűen csak működik.
import SwiftData
@Model
class Book {
var title: String
var author: String
var rating: Int
var isRead: Bool
init(title: String, author: String, rating: Int = 0, isRead: Bool = false) {
self.title = title
self.author = author
self.rating = rating
self.isRead = isRead
}
}
struct BookListView: View {
@Query(sort: \Book.title) private var books: [Book]
@Environment(\.modelContext) private var modelContext
var body: some View {
List(books) { book in
// A Book automatikusan @Observable,
// ezért a BookRowView csak akkor frissül,
// ha az adott könyv tulajdonságai változnak
BookRowView(book: book)
}
}
}
struct BookRowView: View {
@Bindable var book: Book
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
Toggle("Elolvasva", isOn: $book.isRead)
.labelsHidden()
}
}
}
Összetett nézet-hierarchia kezelése
Nagyobb alkalmazásoknál érdemes több, kisebb @Observable osztályt létrehozni egyetlen hatalmas állapotobjektum helyett. Saját tapasztalatom alapján ez nemcsak a teljesítményen segít, hanem a kód karbantarthatóságán is sokat javít:
// Különálló, fókuszált állapotosztályok
@Observable
class AuthState {
var currentUser: User?
var isAuthenticated: Bool { currentUser != nil }
var authToken: String?
}
@Observable
class NavigationState {
var selectedTab: Tab = .home
var navigationPath = NavigationPath()
var isShowingSheet: Bool = false
}
@Observable
class NotificationState {
var notifications: [AppNotification] = []
var unreadCount: Int {
notifications.filter { !$0.isRead }.count
}
}
// Az AppState összefogja őket
@Observable
class AppState {
var auth = AuthState()
var navigation = NavigationState()
var notifications = NotificationState()
}
// A nézetekben csak a szükséges részt adjuk tovább
struct MainTabView: View {
@Environment(AppState.self) private var appState
var body: some View {
@Bindable var nav = appState.navigation
TabView(selection: $nav.selectedTab) {
// A HomeView csak az auth állapotot kapja,
// nem az egész AppState-et
HomeView(auth: appState.auth)
.tabItem { Label("Főoldal", systemImage: "house") }
.tag(Tab.home)
NotificationListView(state: appState.notifications)
.tabItem { Label("Értesítések", systemImage: "bell") }
.badge(appState.notifications.unreadCount)
.tag(Tab.notifications)
}
}
}
Gyakori hibák és azok elkerülése
Az Observation keretrendszer használata során néhány gyakori hibába futhatunk bele. Összeszedtem a leggyakoribbakat – ezeken már sokan megégették magukat.
1. Az @Observable csak osztályokkal működik
A leggyakoribb hiba (különösen, ha gyorsan akarsz haladni): az @Observable makrót struct-ra próbálod alkalmazni. Az Observation keretrendszer kizárólag referencia típusokat támogat:
// HIBÁS – nem fog fordulni!
// @Observable
// struct UserData {
// var name: String = ""
// }
// HELYES – osztályt kell használni
@Observable
class UserData {
var name: String = ""
}
2. A @State inicializáció ismétlődése
Ez egy finom részlet, ami könnyen elkerüli a figyelmedet. Az @State nem használ autoclosure-t az inicializáláshoz, ami azt jelenti, hogy a szülő nézet body-jának minden kiértékelésekor lefut az inicializáló kifejezés – de a SwiftUI elveti az eredményt (csak az első inicializálás számít). Ha az inicializálás költséges, ezt tartsd szem előtt:
struct ParentView: View {
@State private var isShowingChild = false
var body: some View {
Button("Mutasd") {
isShowingChild = true
}
.sheet(isPresented: $isShowingChild) {
// FIGYELEM: A ChildViewModel() MINDEN alkalommal lefut,
// amikor a ParentView body-ja kiértékelődik,
// de a SwiftUI eldobja az eredményt az első után
ChildView()
}
}
}
struct ChildView: View {
@State private var viewModel = ChildViewModel()
// ...
var body: some View { Text("Child") }
}
3. Hiányzó .environment() módosító
Ha elfelejted a .environment() módosítót a nézet-hierarchia tetején, futásidejű hibát kapsz. És higgy nekem, ez nem egy barátságos hibaüzenet lesz:
// Ha ezt elfelejtjük:
// .environment(appState)
// Akkor ez a nézet futásidejű hibát dob:
struct SomeView: View {
@Environment(AppState.self) private var appState // CRASH!
var body: some View { Text("Hiba") }
}
Mindig ellenőrizd, hogy az @Environment-tel használt típusokat a nézet-hierarchia egy magasabb pontján hozzáadtad-e a .environment() módosítóval! Az Xcode Preview-knál is gyakori ez a probléma – a preview-ban is meg kell adnod a környezeti objektumokat.
4. Property wrapperek ütközése
Az @Observable makró a háttérben számított tulajdonságokká alakítja a tárolt tulajdonságokat. Emiatt bizonyos property wrapperek (mint az @AppStorage) nem működnek jól vele:
@Observable
class Settings {
// Ez NEM fog működni a várt módon,
// mert az @AppStorage és az @Observable ütközik
// @AppStorage("isDarkMode") var isDarkMode = false
// Helyette használj manuális megoldást:
var isDarkMode: Bool {
get { UserDefaults.standard.bool(forKey: "isDarkMode") }
set { UserDefaults.standard.set(newValue, forKey: "isDarkMode") }
}
}
Összefoglalás: Melyik property wrappert mikor használjuk?
Végül álljon itt egy gyors összefoglaló, amit akár ki is nyomtathatsz magadnak:
- @State + értéktípus (String, Int, struct): Helyi, a nézet által birtokolt egyszerű állapot
- @State + @Observable osztály: A nézet hozza létre és birtokolja az objektumot (korábbi @StateObject)
- @Bindable: Kívülről kapott @Observable objektum, amelyhez kötés kell (korábbi @ObservedObject)
- Semmilyen wrapper: Kívülről kapott @Observable objektum, amelyet csak olvasunk
- @Environment: Alkalmazásszintű @Observable objektum megosztása a nézet-hierarchiában (korábbi @EnvironmentObject)
- @Binding: Kétirányú kötés egyetlen értékhez (ez nem változott)
A modern SwiftUI állapotkezelés lényegesen egyszerűbb, mint a korábbi rendszer. Az @Observable makró, az egyszerűsített property wrapper készlet és a finom szemcsézettségű frissítések együttesen hatékonyabb, könnyebben karbantartható és jobban teljesítő alkalmazást eredményeznek.
A legfontosabb tanácsom: ha új projektet kezdesz, vagy iOS 17+ a minimum célplatformod, használd bátran az Observation keretrendszert. Ha meglévő projektet migrálsz, tedd fokozatosan, nézet nézetenként – a két rendszer békésen megfér egymás mellett.
Remélem, ez az útmutató segített megérteni az Observation keretrendszer működését és a modern SwiftUI állapotkezelés legjobb gyakorlatait. Jó kódolást!