Observation-frameworket i SwiftUI: Guide til @Observable, @State, @Bindable og @Environment

Komplet guide til Observation-frameworket i SwiftUI. Lær @Observable-makroen, finkornet sporing, @State, @Bindable og @Environment med praktiske kodeeksempler og migreringstips.

Introduktion: Hvorfor Observation-frameworket ændrer alt

Med iOS 17 og Swift 5.9 introducerede Apple Observation-frameworket — og det var ærligt talt på tide. Det er ikke bare en mindre forbedring eller et nyt lag maling. Det er en komplet nytænkning af, hvordan data flyder mellem modeller og brugergrænsefladen i SwiftUI, og det løser problemer, som har plaget udviklere siden SwiftUIs lancering i 2019.

Hvad var problemet egentlig?

Den gamle tilgang med ObservableObject-protokollen og Combine havde alvorlige begrænsninger. Hver gang et enkelt @Published-property blev ændret, fik alle views, der observerede objektet, besked om at opdatere sig — uanset om de faktisk brugte det ændrede property. Det førte til unødvendige genrendereringer, dårlig ydeevne og (lad os være ærlige) frustrerede udviklere, der ikke helt forstod, hvorfor deres app var langsom.

Observation-frameworket løser dette med finkornet sporing (fine-grained tracking). SwiftUI ved nu præcis, hvilke properties et view læser, og opdaterer kun det view, når netop de properties ændres. Resultatet? Markant bedre ydeevne og en langt enklere API.

I denne guide gennemgår vi alle aspekter af Observation-frameworket — fra @Observable-makroen og dens indre funktionsmåde, over @State, @Bindable og @Environment, til migrering fra det gamle system, avancerede mønstre og ydeevneoptimering. Så lad os dykke ned i det.

Hvad er Observation-frameworket?

Observation-frameworket blev præsenteret på WWDC23 og er baseret på Swift Evolution-forslaget SE-0395 (Observation). Det er et selvstændigt Swift-framework, der faktisk ikke er bundet til SwiftUI — det kan bruges i enhver Swift-kontekst — men det er i SwiftUI, at det virkelig skinner.

Frameworkets kerne er @Observable-makroen, som erstatter ObservableObject-protokollen og @Published-property wrapperen. I stedet for at markere hvert enkelt property med @Published, annoterer du blot hele klassen med @Observable, og frameworket klarer resten. Simpelt og elegant.

De vigtigste komponenter i det nye system:

  • @Observable — makro der gør en klasse observerbar med automatisk sporing af alle properties
  • @State — bruges nu til at eje og styre livscyklussen for Observable-objekter (erstatter @StateObject)
  • @Bindable — opretter bindinger til et Observable-objekts properties (erstatter dele af @ObservedObject)
  • @Environment — injicerer Observable-objekter via miljøet (erstatter @EnvironmentObject)
  • @ObservationIgnored — ekskluderer specifikke properties fra sporing

Denne forenkling betyder, at du skal huske færre property wrappere, og at det er sværere at vælge forkert. Det er en kæmpe gevinst — især for folk, der er nye i SwiftUI, men bestemt også for os, der har kæmpet med @StateObject vs. @ObservedObject siden dag ét.

@Observable-makroen i dybden

Makroen @Observable er hjørnestenen i det nye framework. Den bruger Swift 5.9s makrosystem til at transformere din klasse ved kompileringstidspunktet, så alle lagrede properties automatisk spores for læsning og skrivning.

Grundlæggende brug

Lad os starte med et praktisk eksempel — en simpel opgavestyringsapp:

import Observation

@Observable
class TaskStore {
    var tasks: [TaskItem] = []
    var filterText: String = ""
    var showCompletedOnly: Bool = false

    var filteredTasks: [TaskItem] {
        tasks.filter { task in
            let matchesFilter = filterText.isEmpty ||
                task.title.localizedCaseInsensitiveContains(filterText)
            let matchesCompleted = !showCompletedOnly || task.isCompleted
            return matchesFilter && matchesCompleted
        }
    }

    func addTask(title: String) {
        tasks.append(TaskItem(title: title))
    }

    func toggleTask(_ task: TaskItem) {
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks[index].isCompleted.toggle()
        }
    }
}

struct TaskItem: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool = false
}

Bemærk, hvor rent koden er. Ingen @Published på hvert property, ingen konformitet til protokoller. Bare @Observable på klassen, og makroen klarer resten. Ærligt talt føles det næsten for nemt.

Hvad makroen genererer under overfladen

Når Swift-kompileren behandler @Observable-makroen, genererer den en hel del kode bag kulisserne. Hvis du er typen, der gerne vil forstå, hvad der egentlig foregår (og det bør du være!), så er det værd at se nærmere. Her er en forenklet udgave af, hvad makroen genererer for et enkelt property:

// Det du skriver:
@Observable
class TaskStore {
    var filterText: String = ""
}

// Hvad kompileren genererer (forenklet):
class TaskStore: Observable {
    @ObservationTracked
    var filterText: String = ""
    // Bliver til noget i retning af:
    // private var _filterText: String = ""
    // var filterText: String {
    //     get {
    //         access(keyPath: \.filterText)
    //         return _filterText
    //     }
    //     set {
    //         withMutation(keyPath: \.filterText) {
    //             _filterText = newValue
    //         }
    //     }
    // }

    @ObservationIgnored
    private let _$observationRegistrar = ObservationRegistrar()

    internal nonisolated func access<Member>(
        keyPath: KeyPath<TaskStore, Member>
    ) {
        _$observationRegistrar.access(self, keyPath: keyPath)
    }

    internal nonisolated func withMutation<Member, MutationResult>(
        keyPath: KeyPath<TaskStore, Member>,
        _ mutation: () throws -> MutationResult
    ) rethrows -> MutationResult {
        try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
    }
}

Det vigtige at bemærke er de to metoder: access(keyPath:) og withMutation(keyPath:_:). Når et property læses, kalder getteren access, som registrerer, at det pågældende property er blevet tilgået. Når et property skrives, kalder setteren withMutation, som notificerer alle observatører om ændringen.

Denne mekanisme er nøglen til finkornet sporing — systemet ved præcist, hvilke properties der er læst og af hvem. Ret smart, faktisk.

Finkornet sporing: Hvorfor ydeevne forbedres dramatisk

Den største fordel ved Observation-frameworket er uden tvivl finkornet sporing. I stedet for at spore ændringer på objektniveau (som ObservableObject gjorde), sporer det nye framework ændringer på property-niveau.

Lad os illustrere forskellen med et eksempel, der virkelig viser effekten:

@Observable
class AppSettings {
    var username: String = "bruger123"
    var theme: AppTheme = .light
    var notificationsEnabled: Bool = true
    var fontSize: Double = 16.0
    var language: String = "da"
}

// Dette view læser KUN 'username'
struct ProfileHeader: View {
    let settings: AppSettings

    var body: some View {
        Text("Hej, \(settings.username)!")
            .font(.title)
    }
}

// Dette view læser KUN 'theme' og 'fontSize'
struct ContentArea: View {
    let settings: AppSettings

    var body: some View {
        Text("Indhold vises her")
            .font(.system(size: settings.fontSize))
            .foregroundStyle(settings.theme == .dark ? .white : .black)
    }
}

// Dette view læser KUN 'notificationsEnabled'
struct NotificationBadge: View {
    let settings: AppSettings

    var body: some View {
        if settings.notificationsEnabled {
            Image(systemName: "bell.badge.fill")
        } else {
            Image(systemName: "bell.slash")
        }
    }
}

Med Observation-frameworket gælder følgende: Når username ændres, opdateres kun ProfileHeader. Når theme ændres, opdateres kun ContentArea. NotificationBadge forbliver helt uberørt.

Med det gamle ObservableObject-system? Alle tre views ville blive genrenderet ved enhver ændring. Tænk over det i en app med 50+ views, der deler den samme model.

I praksis kan dette reducere antallet af unødvendige genrendereringer med 30-50 % eller mere i komplekse apps. Apples egne benchmarks fra WWDC23 viste markante forbedringer, især i apps med mange views, der deler store datamodeller. Det bedste? Du behøver ikke gøre noget specielt for at opnå denne optimering. Bare brug @Observable, og SwiftUI klarer resten.

@State med @Observable-objekter

I den gamle verden brugte man @StateObject til at oprette og eje en instans af et ObservableObject inden for et view. Med Observation-frameworket er @StateObject ikke længere nødvendig — @State håndterer nu også livscyklussen for Observable-objekter.

Dette er en vigtig pointe, som mange overser: @State sikrer, at objektet overlever genrendereringer af viewet. Uden @State ville objektet blive oprettet på ny, hver gang SwiftUI genopbygger viewet — og det er sjældent, hvad du vil have.

@Observable
class ShoppingCart {
    var items: [CartItem] = []

    var totalPrice: Double {
        items.reduce(0) { $0 + $1.price * Double($1.quantity) }
    }

    var itemCount: Int {
        items.reduce(0) { $0 + $1.quantity }
    }

    func addItem(_ product: Product) {
        if let index = items.firstIndex(where: { $0.productId == product.id }) {
            items[index].quantity += 1
        } else {
            items.append(CartItem(productId: product.id,
                                  name: product.name,
                                  price: product.price))
        }
    }

    func removeItem(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }
}

struct CartItem: Identifiable {
    let id = UUID()
    let productId: String
    let name: String
    let price: Double
    var quantity: Int = 1
}

struct ShoppingCartView: View {
    // @State ejer objektet og sikrer dets livscyklus
    @State private var cart = ShoppingCart()

    var body: some View {
        NavigationStack {
            List {
                ForEach(cart.items) { item in
                    HStack {
                        Text(item.name)
                        Spacer()
                        Text("\(item.quantity) x \(item.price, format: .currency(code: "DKK"))")
                    }
                }
                .onDelete(perform: cart.removeItem)
            }
            .navigationTitle("Indkøbskurv (\(cart.itemCount))")
            .toolbar {
                ToolbarItem(placement: .bottomBar) {
                    Text("Total: \(cart.totalPrice, format: .currency(code: "DKK"))")
                        .bold()
                }
            }
        }
    }
}

Reglen er egentlig ret simpel: Hvis dit view opretter og ejer objektet, brug @State. Hvis viewet modtager objektet udefra (som parameter eller via miljøet), behøver du ikke @State — en regulær let- eller var-property duer fint, eller @Bindable hvis du har brug for bindinger.

@Bindable: Opret bindinger til Observable-egenskaber

@Bindable er en ny property wrapper introduceret sammen med Observation-frameworket. Den opretter two-way bindinger til properties på et Observable-objekt og bruges typisk, når du har brug for at videregive en Binding til SwiftUI-kontroller som TextField, Toggle eller Slider.

Her er et eksempel med en profilredigeringsskærm:

@Observable
class UserProfile {
    var name: String = ""
    var email: String = ""
    var bio: String = ""
    var receiveNewsletter: Bool = true
    var maxNotificationsPerDay: Double = 5
}

struct ProfileEditView: View {
    // @Bindable giver adgang til $-syntaksen for bindinger
    @Bindable var profile: UserProfile

    var body: some View {
        Form {
            Section("Personlige oplysninger") {
                TextField("Navn", text: $profile.name)
                TextField("E-mail", text: $profile.email)
                TextField("Bio", text: $profile.bio, axis: .vertical)
                    .lineLimit(3...6)
            }

            Section("Notifikationer") {
                Toggle("Modtag nyhedsbrev", isOn: $profile.receiveNewsletter)

                if profile.receiveNewsletter {
                    Slider(
                        value: $profile.maxNotificationsPerDay,
                        in: 1...20,
                        step: 1
                    ) {
                        Text("Maks notifikationer per dag")
                    }
                    Text("Maks \(Int(profile.maxNotificationsPerDay)) notifikationer om dagen")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
        }
    }
}

// Brug fra et overordnet view:
struct SettingsView: View {
    @State private var profile = UserProfile()

    var body: some View {
        NavigationStack {
            ProfileEditView(profile: profile)
                .navigationTitle("Rediger profil")
        }
    }
}

Bemærk mønstret her: Det overordnede view (SettingsView) bruger @State til at eje objektet. Det underordnede view (ProfileEditView) modtager objektet og bruger @Bindable for at kunne oprette bindinger med $-syntaksen. Det er et mønster, du vil se igen og igen.

Hvis du ikke har brug for bindinger, men blot vil læse fra et Observable-objekt, kan du bruge en helt almindelig let-property:

struct ProfileSummaryView: View {
    // Ingen property wrapper nødvendig - bare læsning
    let profile: UserProfile

    var body: some View {
        VStack(alignment: .leading) {
            Text(profile.name).font(.headline)
            Text(profile.email).foregroundStyle(.secondary)
        }
    }
}

@Environment med Observable-typer

@Environment har fået en ny overloaded initializer, der arbejder direkte med Observable-typer. Den erstatter @EnvironmentObject og giver en mere type-sikker måde at injicere afhængigheder på tværs af view-hierarkiet.

Her er et eksempel med en autentificeringsmanager — noget næsten alle apps har brug for:

@Observable
class AuthManager {
    var currentUser: User?
    var isAuthenticated: Bool { currentUser != nil }
    var isLoading: Bool = false

    func signIn(email: String, password: String) async throws {
        isLoading = true
        defer { isLoading = false }
        // Simuler netværkskald
        try await Task.sleep(for: .seconds(1))
        currentUser = User(name: "Lars Hansen", email: email)
    }

    func signOut() {
        currentUser = nil
    }
}

struct User: Identifiable {
    let id = UUID()
    let name: String
    let email: String
}

// Injicer i app-roden:
@main
struct MyStoreApp: App {
    @State private var authManager = AuthManager()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(authManager)
        }
    }
}

// Brug i et vilkårligt underview:
struct AccountView: View {
    @Environment(AuthManager.self) private var authManager

    var body: some View {
        if let user = authManager.currentUser {
            VStack(spacing: 16) {
                Text("Velkommen, \(user.name)!")
                    .font(.title2)
                Text(user.email)
                    .foregroundStyle(.secondary)

                Button("Log ud", role: .destructive) {
                    authManager.signOut()
                }
            }
        } else {
            LoginView()
        }
    }
}

struct LoginView: View {
    @Environment(AuthManager.self) private var authManager
    @State private var email = ""
    @State private var password = ""

    var body: some View {
        Form {
            TextField("E-mail", text: $email)
                .textContentType(.emailAddress)
            SecureField("Adgangskode", text: $password)

            Button("Log ind") {
                Task {
                    try? await authManager.signIn(
                        email: email,
                        password: password
                    )
                }
            }
            .disabled(email.isEmpty || password.isEmpty)
        }
    }
}

En vigtig forskel fra @EnvironmentObject: Du angiver nu typen direkte i @Environment-initializeren — @Environment(AuthManager.self) — i stedet for at bruge et key path. Dette giver bedre type-sikkerhed og (endelig!) tydeligere fejlmeddelelser, hvis objektet mangler i miljøet.

Har du brug for bindinger til et Environment-objekt? Du kan kombinere @Environment med @Bindable lokalt:

struct SettingsFormView: View {
    @Environment(AppSettings.self) private var settings

    var body: some View {
        @Bindable var settings = settings

        Form {
            Toggle("Mørk tilstand", isOn: $settings.darkModeEnabled)
            Slider(value: $settings.fontSize, in: 12...24)
        }
    }
}

Det er lidt uvant at erklære @Bindable inde i body, men det virker og er den anbefalede tilgang fra Apple.

@ObservationIgnored: Kontrol over hvad der spores

Ikke alle properties i en @Observable-klasse skal nødvendigvis spores. Nogle properties er interne implementeringsdetaljer, caches eller konstanter, der aldrig ændres. Til dette formål findes @ObservationIgnored.

@Observable
class DocumentEditor {
    var title: String = "Nyt dokument"
    var content: String = ""
    var lastSaved: Date?

    // Disse properties spores IKKE af Observation-frameworket
    @ObservationIgnored
    let createdAt = Date()

    @ObservationIgnored
    var internalCache: [String: Any] = [:]

    @ObservationIgnored
    private var saveTimer: Timer?

    @ObservationIgnored
    var undoStack: [String] = []

    var wordCount: Int {
        content.split(separator: " ").count
    }

    func save() {
        lastSaved = Date()
        // Gem indhold...
    }

    func startAutoSave() {
        saveTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in
            self?.save()
        }
    }
}

Hvornår bør du bruge @ObservationIgnored?

  • Konstanter der aldrig ændres (let-properties er automatisk ignoreret, men det skader ikke at være eksplicit)
  • Interne caches der ikke bør udløse UI-opdateringer
  • Timere, cancellables og andre infrastrukturobjekter
  • Properties der ændres ekstremt hyppigt — f.eks. en animation-progress-værdi, der opdateres 60 gange i sekundet. Du vil bestemt ikke have SwiftUI til at genrendere for hver enkelt frame.

Migrering fra ObservableObject til @Observable

Har du en eksisterende SwiftUI-app med ObservableObject? Bare rolig — migrering til @Observable er forholdsvis ligetil. Her er en trin-for-trin guide.

Trin 1: Omdannelse af modellen

// FØR: ObservableObject med @Published
class ProjectManager: ObservableObject {
    @Published var projects: [Project] = []
    @Published var selectedProject: Project?
    @Published var isLoading: Bool = false
    @Published var errorMessage: String?

    func loadProjects() async {
        isLoading = true
        defer { isLoading = false }
        // Hent projekter...
    }
}

// EFTER: @Observable
@Observable
class ProjectManager {
    var projects: [Project] = []
    var selectedProject: Project?
    var isLoading: Bool = false
    var errorMessage: String?

    func loadProjects() async {
        isLoading = true
        defer { isLoading = false }
        // Hent projekter...
    }
}

Ser du forskellen? Fjern ObservableObject-konformiteten, fjern alle @Published-annoteringer, og tilføj @Observable til klassen. Det er det.

Trin 2: Opdatering af views

Her er de vigtigste ændringer i dine views:

// FØR
struct ProjectListView: View {
    @StateObject private var manager = ProjectManager()

    var body: some View {
        List(manager.projects) { project in
            ProjectRow(project: project)
        }
    }
}

// EFTER: @StateObject → @State
struct ProjectListView: View {
    @State private var manager = ProjectManager()

    var body: some View {
        List(manager.projects) { project in
            ProjectRow(project: project)
        }
    }
}
// FØR
struct ProjectDetailView: View {
    @ObservedObject var manager: ProjectManager

    var body: some View {
        if let project = manager.selectedProject {
            Text(project.name)
        }
    }
}

// EFTER: @ObservedObject → let (kun læsning) eller @Bindable (med bindinger)
struct ProjectDetailView: View {
    var manager: ProjectManager  // bare en regulær property

    var body: some View {
        if let project = manager.selectedProject {
            Text(project.name)
        }
    }
}
// FØR
struct SidebarView: View {
    @EnvironmentObject var manager: ProjectManager

    var body: some View {
        List(manager.projects) { project in
            Text(project.name)
        }
    }
}

// EFTER: @EnvironmentObject → @Environment(Type.self)
struct SidebarView: View {
    @Environment(ProjectManager.self) private var manager

    var body: some View {
        List(manager.projects) { project in
            Text(project.name)
        }
    }
}

// Og injektion ændres fra:
// .environmentObject(manager)
// til:
// .environment(manager)

Den hurtige migreringsreference

Her er en oversigt over alle erstatninger, du kan printe ud og hænge ved skærmen:

  • ObservableObject@Observable
  • @Published varvar (ingen annotation nødvendig)
  • @StateObject@State
  • @ObservedObjectlet / var (kun læsning) eller @Bindable (med bindinger)
  • @EnvironmentObject@Environment(Type.self)
  • .environmentObject(_:).environment(_:)

withObservationTracking: Manuel observation

Observation-frameworket er ikke begrænset til SwiftUI. Med funktionen withObservationTracking(_:onChange:) kan du manuelt observere ændringer i enhver kontekst — f.eks. i UIKit, baggrundstjenester eller tests.

@Observable
class SensorData {
    var temperature: Double = 20.0
    var humidity: Double = 45.0
    var pressure: Double = 1013.25
}

// Manuel observation uden for SwiftUI
let sensorData = SensorData()

func startMonitoring() {
    withObservationTracking {
        // Alt der tilgås her, spores
        print("Temperatur: \(sensorData.temperature)°C")
        print("Luftfugtighed: \(sensorData.humidity)%")
    } onChange: {
        // Kaldes når ET af de sporede properties ændres
        // VIGTIGT: Dette kaldes kun ÉN gang!
        print("Sensordata er ændret!")

        // For løbende observation: Kald funktionen igen
        DispatchQueue.main.async {
            startMonitoring()
        }
    }
}

// Kontinuerlig observation med async/await
func observeTemperature() async {
    let sensorData = SensorData()

    while !Task.isCancelled {
        let currentTemp = await withCheckedContinuation { continuation in
            withObservationTracking {
                _ = sensorData.temperature
            } onChange: {
                continuation.resume(returning: sensorData.temperature)
            }
        }

        if currentTemp > 30.0 {
            print("Advarsel: Høj temperatur (\(currentTemp)°C)!")
        }
    }
}

En vigtig detalje, der er let at overse: onChange-closuren kaldes kun én gang. Hvis du vil have løbende observation, skal du kalde funktionen igen inde i onChange-handleren. SwiftUI gør dette automatisk bag kulisserne — det er derfor dine views altid er opdaterede — men i manuelle scenarier skal du selv håndtere det.

Avancerede mønstre og bedste praksis

Indlejrede Observable-objekter

Når du har Observable-objekter, der indeholder andre Observable-objekter, virker sporingen korrekt på tværs af niveauer. SwiftUI sporer præcis de properties, du faktisk tilgår:

@Observable
class Company {
    var name: String
    var address: Address
    var employees: [Employee] = []

    init(name: String, address: Address) {
        self.name = name
        self.address = address
    }
}

@Observable
class Address {
    var street: String
    var city: String
    var postalCode: String

    init(street: String, city: String, postalCode: String) {
        self.street = street
        self.city = city
        self.postalCode = postalCode
    }
}

@Observable
class Employee {
    var name: String
    var title: String

    init(name: String, title: String) {
        self.name = name
        self.title = title
    }
}

struct CompanyView: View {
    let company: Company

    var body: some View {
        VStack(alignment: .leading) {
            // Sporer company.name
            Text(company.name).font(.title)

            // Sporer company.address.city og company.address.postalCode
            Text("\(company.address.postalCode) \(company.address.city)")
                .foregroundStyle(.secondary)

            // Sporer company.employees (arrayet selv)
            Text("\(company.employees.count) medarbejdere")
        }
    }
}

Dog er der en vigtig nuance at være opmærksom på: Når du har et array af Observable-objekter, sporer SwiftUI selve arrayet (tilføjelser og fjernelser), men for at spore ændringer inden i de enkelte objekter, skal viewet faktisk tilgå de relevante properties. I eksemplet ovenfor sporer CompanyView kun employees.count — ikke navnene på de enkelte medarbejdere. Et separat view, der viser medarbejdernavne, ville automatisk spore disse.

Computed properties

Computed properties fungerer fuldstændig problemfrit med Observation-frameworket. Sporingsmekanismen følger afhængighedskæden automatisk, og det er en af de ting, der bare virker:

@Observable
class Invoice {
    var items: [InvoiceItem] = []
    var taxRate: Double = 0.25
    var discountPercentage: Double = 0

    // Computed property - sporer automatisk 'items'
    var subtotal: Double {
        items.reduce(0) { $0 + $1.amount }
    }

    // Sporer 'subtotal' (og dermed 'items') samt 'discountPercentage'
    var discountAmount: Double {
        subtotal * (discountPercentage / 100)
    }

    // Sporer 'subtotal', 'discountAmount' og 'taxRate'
    var total: Double {
        let afterDiscount = subtotal - discountAmount
        return afterDiscount * (1 + taxRate)
    }
}

struct InvoiceItem: Identifiable {
    let id = UUID()
    var description: String
    var amount: Double
}

Når et view læser invoice.total, sporer SwiftUI automatisk alle de underliggende properties, som total afhænger af. Ændring af taxRate, discountPercentage eller en tilføjelse til items vil alle udløse en opdatering. Du behøver ikke tænke over det — det bare virker.

Almindelige faldgruber

Lad mig nævne de fejl, jeg ser oftest (og som jeg selv har begået):

  1. At glemme @State for ejede objekter: Hvis dit view opretter et Observable-objekt uden @State, vil objektet blive genskabt ved hver genrendering. Det er en subtil fejl, der kan give meget mærkelig opførsel.
  2. At bruge @Observable på structs: Makroen virker kun på klasser, da den kræver referencesemantics og identitet. Brug struct med @State for værdi-baserede tilstande.
  3. At blande gamle og nye mønstre: Undgå at bruge @ObservedObject med @Observable-klasser. Vælg ét system og brug det konsekvent.

Ydeevneoptimering med Observation

Selvom Observation-frameworket automatisk giver bedre ydeevne end ObservableObject, kan du optimere yderligere med bevidste designvalg. Her er et par strategier, der virkelig gør en forskel.

Opdeling af store modeller

Hvis du har et stort model-objekt med mange properties, kan det give god mening at opdele det i mindre, fokuserede Observable-objekter:

// I stedet for én stor model:
@Observable
class AppState {
    // Brugerdata
    var username: String = ""
    var avatar: Data?
    // Indstillinger
    var theme: Theme = .system
    var language: String = "da"
    // Indkøbskurv
    var cartItems: [CartItem] = []
    var promoCode: String?
    // Notifikationer
    var unreadCount: Int = 0
    var notifications: [Notification] = []
}

// Opdel i fokuserede modeller:
@Observable
class UserState {
    var username: String = ""
    var avatar: Data?
}

@Observable
class SettingsState {
    var theme: Theme = .system
    var language: String = "da"
}

@Observable
class CartState {
    var items: [CartItem] = []
    var promoCode: String?

    var totalPrice: Double {
        items.reduce(0) { $0 + $1.price * Double($1.quantity) }
    }
}

@Observable
class NotificationState {
    var unreadCount: Int = 0
    var notifications: [AppNotification] = []
}

Denne opdeling giver mere end blot ydeevnefordele — den forbedrer også kodens læsbarhed, testbarhed og vedligeholdelse. Hvert objekt har et klart ansvarsområde og kan udvikles uafhængigt. Det er god softwarearkitektur, uanset om du bruger Observation-frameworket eller ej.

View-dekomposition

Kombiner modelopdeling med view-dekomposition for maksimal ydeevne. Bryd store views op i mindre under-views, der kun læser de properties, de rent faktisk bruger:

// Godt mønster: Hvert under-view læser kun relevante properties
struct ProductPageView: View {
    @State private var product = ProductViewModel()

    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                ProductImageGallery(product: product)
                ProductInfoSection(product: product)
                ProductReviewsSection(product: product)
                AddToCartButton(product: product)
            }
        }
    }
}

// Kun 'images' spores
struct ProductImageGallery: View {
    let product: ProductViewModel

    var body: some View {
        TabView {
            ForEach(product.images, id: \.self) { imageUrl in
                AsyncImage(url: URL(string: imageUrl)) { image in
                    image.resizable().scaledToFit()
                } placeholder: {
                    ProgressView()
                }
            }
        }
        .tabViewStyle(.page)
        .frame(height: 300)
    }
}

// Kun 'name', 'price' og 'description' spores
struct ProductInfoSection: View {
    let product: ProductViewModel

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(product.name).font(.title2).bold()
            Text(product.price, format: .currency(code: "DKK"))
                .font(.title3)
                .foregroundStyle(.blue)
            Text(product.description)
                .foregroundStyle(.secondary)
        }
        .padding(.horizontal)
    }
}

@Observable
class ProductViewModel {
    var name: String = "Swift Programming Book"
    var price: Double = 299.00
    var description: String = "Den komplette guide til Swift-programmering"
    var images: [String] = []
    var reviews: [Review] = []
    var isInCart: Bool = false
    var selectedQuantity: Int = 1
}

struct Review: Identifiable {
    let id = UUID()
    let author: String
    let rating: Int
    let text: String
}

Med denne tilgang genrenderer SwiftUI kun ProductInfoSection, når prisen ændres, og kun ProductImageGallery, når billederne ændres. Resten af siden forbliver helt uberørt. I en app med mange produktsider kan det give en mærkbar forskel i responsivitet.

Konklusion

Observation-frameworket er, efter min mening, en af de mest betydningsfulde forbedringer i SwiftUIs historie. Det forenkler API'en drastisk, fjerner behovet for de forvirrende property wrappere som @StateObject og @EnvironmentObject, og leverer markant bedre ydeevne gennem finkornet sporing — alt sammen uden at du skal gøre noget ekstra.

De vigtigste pointer fra denne guide:

  • @Observable erstatter ObservableObject og @Published med en enkelt makro
  • Finkornet sporing sikrer, at kun views der faktisk bruger et ændret property, genrenderes
  • @State erstatter @StateObject for at eje Observable-objekter
  • @Bindable opretter bindinger til Observable-properties
  • @Environment(Type.self) erstatter @EnvironmentObject med bedre type-sikkerhed
  • @ObservationIgnored giver kontrol over, hvilke properties der spores
  • withObservationTracking muliggør observation uden for SwiftUI

Migrering fra det gamle system er overkommelig og kan heldigvis gøres trinvist — du behøver ikke omskrive hele din app på én gang. Start med nye features, og migrer eksisterende kode, når det giver mening.

Observation-frameworket er kommet for at blive. Jo før du adopterer det, jo bedre bliver din kode — og din apps ydeevne. God fornøjelse med at bygge hurtigere, enklere SwiftUI-apps.

Om Forfatteren Editorial Team

Our team of expert writers and editors.