SwiftUI állapotkezelés az Observation keretrendszerrel: Útmutató az @Observable, @State, @Bindable és @Environment használatához

Gyakorlati útmutató a SwiftUI Observation keretrendszeréhez: tanuld meg az @Observable, @State, @Bindable és @Environment helyes használatát, a migrációs lépéseket és a gyakori hibák elkerülését kódpéldákkal.

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:

  1. ObservableObject@Observable makró
  2. @Published → Töröld (nem kell semmi helyette!)
  3. @StateObject@State
  4. @ObservedObject (ha kötésre van szükség) → @Bindable
  5. @ObservedObject (ha csak olvasás) → Semmilyen wrapper
  6. @EnvironmentObject@Environment(TypusNev.self)
  7. .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!

A Szerzőről Editorial Team

Our team of expert writers and editors.