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 var→var(ingen annotation nødvendig)@StateObject→@State@ObservedObject→let/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):
- At glemme
@Statefor 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. - At bruge
@Observablepå structs: Makroen virker kun på klasser, da den kræver referencesemantics og identitet. Brugstructmed@Statefor værdi-baserede tilstande. - At blande gamle og nye mønstre: Undgå at bruge
@ObservedObjectmed@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:
@ObservableerstatterObservableObjectog@Publishedmed en enkelt makro- Finkornet sporing sikrer, at kun views der faktisk bruger et ændret property, genrenderes
@Stateerstatter@StateObjectfor at eje Observable-objekter@Bindableopretter bindinger til Observable-properties@Environment(Type.self)erstatter@EnvironmentObjectmed bedre type-sikkerhed@ObservationIgnoredgiver kontrol over, hvilke properties der sporeswithObservationTrackingmuliggø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.