@Observable i Swift: Komplett Guide till Observation-ramverket och Modern Tillståndshantering

Observation-ramverket och @Observable har förändrat tillståndshantering i SwiftUI i grunden. Denna guide tar dig från grunderna till avancerade mönster — inklusive @Bindable, prestandafördelar, migrering från ObservableObject och bästa praxis.

Introduktion: En ny era för tillståndshantering i SwiftUI

Ända sedan SwiftUI lanserades 2019 har tillståndshantering — hur data flödar genom din app och utlöser vyuppdateringar — varit ramverkets absolut viktigaste byggsten. Och ärligt talat, också den mest diskuterade. Genom åren har vi gått från @ObservedObject och @StateObject till en helt ny verklighet: Observation-ramverket och @Observable-makrot.

Det här är inte bara ett enklare API. Det är en fundamental förändring i hur SwiftUI avgör vilka vyer som behöver ritas om.

I den här artikeln går vi igenom allt du behöver veta — från grunderna i @Observable, genom avancerade mönster med @Bindable och @Environment, till prestanda-optimeringar, fallgropar och hur du migrerar befintliga projekt. Vi tittar också på hur Observation-ramverket fungerar utanför SwiftUI och hur det samverkar med Swift 6:s concurrency-modell.

Problemet med ObservableObject

Innan vi dyker in i det nya behöver vi förstå varför Apple bestämde sig för att ersätta det gamla systemet. ObservableObject i kombination med Combine-ramverket har tjänat oss väl sedan SwiftUI:s tidiga dagar, men det hade flera begränsningar som blev allt mer kännbara i takt med att appar växte.

Push-baserad invalidering: Problemets kärna

ObservableObject använder en push-baserad invalideringsmodell. Det innebär att objektet sänder ut en generisk "jag har ändrats"-signal via sin objectWillChange-publisher varje gång någon @Published-egenskap ändras. Problemet? SwiftUI har ingen aning om vilken egenskap som ändrades. Allt ramverket vet är att något förändrats, och därför måste det rita om alla vyer som observerar objektet — oavsett om de faktiskt använder den ändrade egenskapen eller inte.

// Det gamla sättet med ObservableObject
class UserProfile: ObservableObject {
    @Published var name: String = "Anna"
    @Published var email: String = "[email protected]"
    @Published var avatarURL: URL?
    @Published var notificationCount: Int = 0
}

struct ProfileHeader: View {
    @ObservedObject var profile: UserProfile

    var body: some View {
        // Denna vy ritas om ÄVEN när notificationCount ändras
        // trots att den bara läser name och avatarURL
        HStack {
            AsyncImage(url: profile.avatarURL)
            Text(profile.name)
        }
    }
}

I exemplet ovan ritas ProfileHeader om varje gång notificationCount uppdateras — trots att vyn aldrig visar den egenskapen. I en liten app märks det knappt. Men i en komplex app med många vyer och frekvent uppdaterade modeller? Då kan det bli riktigt kännbart.

Kodbulk och boilerplate

Det gamla systemet krävde dessutom en ansenlig mängd boilerplate-kod. Varje egenskap som skulle observeras behövde markeras med @Published. Vyer behövde använda specifika property wrappers — @StateObject för ägda objekt, @ObservedObject för vidarebefordrade objekt, och @EnvironmentObject för miljöobjekt. Att välja fel wrapper kunde leda till subtila buggar, som att ett objekt oavsiktligt återskapades vid vyuppdateringar.

Om du nånsin har suttit och stirrat på en bugg i en halvtimme bara för att inse att du använde @ObservedObject istället för @StateObject — ja, du är inte ensam.

Observation-ramverket: Grunderna

Observation-ramverket introducerades vid WWDC 2023 (Swift 5.9) genom SE-0395 och har sedan dess mognat till det rekommenderade sättet att hantera tillstånd i SwiftUI. Ramverkets kärna är @Observable-makrot, som fundamentalt förändrar hur observering fungerar.

Från push till pull: Åtkomstspårning

Den viktigaste skillnaden mot ObservableObject är att Observation-ramverket använder en pull-baserad modell med åtkomstspårning (access tracking). Istället för att objektet sänder ut ändringsmeddelanden, registrerar ramverket vilka egenskaper som faktiskt läses av en vy. När en egenskap sedan ändras meddelar ramverket bara de vyer som läste just den egenskapen.

Det är en elegant lösning, och i praktiken innebär det enorm skillnad.

// Det nya sättet med @Observable
@Observable
class UserProfile {
    var name: String = "Anna"
    var email: String = "[email protected]"
    var avatarURL: URL?
    var notificationCount: Int = 0
}

struct ProfileHeader: View {
    var profile: UserProfile

    var body: some View {
        // Denna vy ritas BARA om när name eller avatarURL ändras
        // Ändringar i notificationCount påverkar inte denna vy!
        HStack {
            AsyncImage(url: profile.avatarURL)
            Text(profile.name)
        }
    }
}

Märk skillnaden: vi behöver varken @Published på egenskaperna eller @ObservedObject i vyn. @Observable-makrot hanterar all observeringslogik automatiskt, och SwiftUI vet exakt vilka egenskaper vyn beror på. Mindre kod, bättre resultat.

Vad @Observable-makrot faktiskt gör

Under huven omvandlar @Observable-makrot dina lagrade egenskaper till beräknade egenskaper som anropar ramverkets spårningssystem. Det är ganska snyggt om man tittar på vad makrot genererar (förenklat):

// Vad @Observable genererar (förenklat)
class UserProfile: Observable {
    @ObservationIgnored private var _name: String = "Anna"

    var name: String {
        get {
            access(keyPath: \.name)  // Registrera åtkomst
            return _name
        }
        set {
            withMutation(keyPath: \.name) {  // Meddela om ändring
                _name = newValue
            }
        }
    }

    // Samma mönster för alla andra egenskaper...

    @ObservationIgnored private let _$observationRegistrar =
        ObservationRegistrar()

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

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

Varje gång en egenskap läses anropas access(keyPath:), och varje gång en egenskap skrivs till anropas withMutation(keyPath:_:). På så vis bygger ramverket upp en exakt bild av vilka egenskaper som är relevanta för varje observatör.

Nya property wrappers: @State, @Bindable och @Environment

En av de mest välkomna förändringarna med Observation-ramverket är att vi inte längre behöver en uppsjö av olika property wrappers. Det är en befrielse, helt ärligt.

@State ersätter @StateObject

Med @Observable-klasser använder du @State istället för @StateObject för att äga ett objekt i en vy:

struct ContentView: View {
    @State private var viewModel = ContentViewModel()

    var body: some View {
        VStack {
            Text(viewModel.title)
            Button("Uppdatera") {
                viewModel.refresh()
            }
        }
    }
}

@Observable
class ContentViewModel {
    var title: String = "Välkommen"
    var items: [String] = []

    func refresh() {
        title = "Uppdaterad!"
        items = ["Objekt 1", "Objekt 2"]
    }
}

En viktig detalj att vara medveten om: @StateObject använde en @autoclosure för sin initiala value, vilket innebar att objektet bara skapades en enda gång. @State med en referenstyp (klass) anropar tekniskt sett initialiseraren varje gång föräldern ritar om sin body — men SwiftUI kastar bort det nya objektet och behåller det ursprungliga. Resultatet är detsamma, men det innebär att tunga initieringar i init() bör undvikas. Använd istället .task-modifieraren för asynkron initiering.

@Bindable: Skapa bindningar från @Observable

Den nya @Bindable-property wrappern ersätter det gamla mönstret med @ObservedObject och $-syntax för att skapa bindningar. Du kan använda den på flera sätt:

@Observable
class EditableProfile {
    var name: String = ""
    var bio: String = ""
    var isPublic: Bool = true
}

// Alternativ 1: @Bindable som property wrapper
struct ProfileEditor: View {
    @Bindable var profile: EditableProfile

    var body: some View {
        Form {
            TextField("Namn", text: $profile.name)
            TextField("Bio", text: $profile.bio)
            Toggle("Offentlig profil", isOn: $profile.isPublic)
        }
    }
}

// Alternativ 2: @Bindable som lokal variabel i body
struct ProfileEditorAlternative: View {
    var profile: EditableProfile

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

        Form {
            TextField("Namn", text: $profile.name)
            TextField("Bio", text: $profile.bio)
            Toggle("Offentlig profil", isOn: $profile.isPublic)
        }
    }
}

Det andra alternativet — att skapa en lokal @Bindable-variabel i body — är särskilt användbart när du hämtar objektet från miljön eller när du itererar över en samling observerbara objekt i en ForEach.

@Environment ersätter @EnvironmentObject

Du kan nu injicera @Observable-objekt direkt i miljön med den vanliga .environment()-modifieraren, utan att behöva använda @EnvironmentObject:

@Observable
class AppSettings {
    var theme: Theme = .light
    var fontSize: CGFloat = 16
    var language: String = "sv"
}

// Injicera i miljön
struct MyApp: App {
    @State private var settings = AppSettings()

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

// Hämta från miljön
struct SettingsView: View {
    @Environment(AppSettings.self) private var settings

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

        Form {
            Picker("Tema", selection: $settings.theme) {
                Text("Ljust").tag(Theme.light)
                Text("Mörkt").tag(Theme.dark)
            }
            Slider(value: $settings.fontSize, in: 12...24) {
                Text("Textstorlek: \(Int(settings.fontSize))")
            }
        }
    }
}

Notera mönstret: vi hämtar settings med @Environment och skapar sedan en lokal @Bindable-variabel för att kunna skapa bindningar. Det här är ett vanligt och rekommenderat mönster för redigerbara miljöobjekt, och något du snabbt vänjer dig vid.

Prestandafördelar: Mätbara skillnader

Den kanske mest övertygande anledningen att migrera till @Observable är prestandaförbättringarna. Tack vare åtkomstspårningen kan SwiftUI vara mycket mer kirurgisk i sina vyuppdateringar.

Egenskapsnivåspårning i praktiken

Betrakta följande scenario med en lista av 1000 objekt:

@Observable
class TodoItem: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool
    var priority: Priority

    init(title: String, isCompleted: Bool = false, priority: Priority = .medium) {
        self.title = title
        self.isCompleted = isCompleted
        self.priority = priority
    }
}

@Observable
class TodoList {
    var items: [TodoItem] = []
    var filterText: String = ""
    var selectedPriority: Priority?

    var filteredItems: [TodoItem] {
        items.filter { item in
            (filterText.isEmpty || item.title.localizedCaseInsensitiveContains(filterText)) &&
            (selectedPriority == nil || item.priority == selectedPriority)
        }
    }
}

struct TodoListView: View {
    @State private var todoList = TodoList()

    var body: some View {
        NavigationStack {
            VStack {
                TextField("Sök...", text: $todoList.filterText)
                    .textFieldStyle(.roundedBorder)
                    .padding()

                List(todoList.filteredItems) { item in
                    TodoRow(item: item)
                }
            }
        }
    }
}

struct TodoRow: View {
    var item: TodoItem

    var body: some View {
        // Denna vy ritas BARA om när just DETTA items
        // title eller isCompleted ändras
        HStack {
            Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
            Text(item.title)
                .strikethrough(item.isCompleted)
        }
    }
}

Med det gamla ObservableObject-systemet skulle en ändring av isCompleted på ett enskilt objekt potentiellt trigga omritning av hela listan. Med @Observable ritas bara den enskilda TodoRow om vars objekt faktiskt ändrades. Utvecklare har i tester rapporterat prestandaförbättringar på 40–60% i vyuppdateringar för komplexa listor. Det är inte siffror man ignorerar.

Spårning av beräknade egenskaper

Observation-ramverket hanterar beräknade egenskaper på ett intelligent sätt. Om en beräknad egenskap beror på flera lagrade egenskaper spårar ramverket de underliggande lagrade egenskaperna:

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

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

    var discount: Double {
        guard discountCode != nil else { return 0 }
        return subtotal * 0.1
    }

    var total: Double {
        subtotal - discount
    }
}

struct CartSummary: View {
    var cart: ShoppingCart

    var body: some View {
        // Spårar: items (via subtotal/total) och discountCode (via discount/total)
        VStack(alignment: .trailing) {
            Text("Delsumma: \(cart.subtotal, format: .currency(code: "SEK"))")
            if cart.discount > 0 {
                Text("Rabatt: -\(cart.discount, format: .currency(code: "SEK"))")
                    .foregroundStyle(.green)
            }
            Text("Totalt: \(cart.total, format: .currency(code: "SEK"))")
                .font(.headline)
        }
    }
}

Ramverket "ser igenom" de beräknade egenskaperna och spårar de faktiska lagrade egenskaperna (items och discountCode) som påverkar resultatet. Vyn ritas bara om när dessa underliggande värden ändras.

@ObservationIgnored: Kontrollera vad som spåras

Ibland har du egenskaper i en @Observable-klass som inte ska trigga vyuppdateringar. Kanske är det cachad data, intern state, eller tunga objekt som inte påverkar UI. Här kommer @ObservationIgnored in i bilden:

@Observable
class MediaPlayer {
    var currentTrack: Track?
    var isPlaying: Bool = false
    var volume: Double = 0.7

    // Dessa egenskaper spåras INTE av Observation
    @ObservationIgnored var audioEngine = AudioEngine()
    @ObservationIgnored var playbackHistory: [Track] = []
    @ObservationIgnored private var internalTimer: Timer?
}

Egenskaper markerade med @ObservationIgnored beter sig som vanliga egenskaper — de genererar inga access()- eller withMutation()-anrop. Det är också nödvändigt att använda detta attribut med lazy-egenskaper, eftersom @Observable-makrot inte kan hantera lazy direkt.

Lazy-egenskaper med @ObservationIgnored

@Observable
class DataProcessor {
    var rawData: [String] = []

    // Fungerar INTE: lazy + @Observable konfliktar
    // lazy var processor = ExpensiveProcessor()

    // Lösning: Använd @ObservationIgnored
    @ObservationIgnored lazy var processor = ExpensiveProcessor()

    // Eller använd explicit initiering
    @ObservationIgnored private var _cache: ProcessedCache?
    var cache: ProcessedCache {
        if _cache == nil {
            _cache = ProcessedCache(data: rawData)
        }
        return _cache!
    }
}

Tänk på att om du använder @ObservationIgnored på en egenskap som din vy läser, kommer vyn inte att uppdateras när den egenskapen ändras. Så använd attributet medvetet — var säker på att vyerna du bygger inte beror på den ignorerade egenskapen.

Observation utanför SwiftUI: withObservationTracking

Observation-ramverket är inte begränsat till SwiftUI, vilket är en av dess riktigt starka sidor. Du kan använda det i ren Swift-kod, UIKit, eller var som helst du behöver reagera på egenskapsändringar. Nyckelfunktionen är withObservationTracking(_:onChange:):

@Observable
class NetworkMonitor {
    var isConnected: Bool = true
    var connectionType: ConnectionType = .wifi
    var signalStrength: Int = 100
}

// Användning i ren Swift
let monitor = NetworkMonitor()

withObservationTracking {
    // Allt som läses här registreras för spårning
    print("Ansluten: \(monitor.isConnected)")
    print("Typ: \(monitor.connectionType)")
} onChange: {
    // Anropas EXAKT EN GÅNG när någon spårad egenskap ändras
    print("Nätverksstatus har ändrats!")
}

Viktigt att notera: onChange-blocket anropas bara en gång. Om du vill fortsätta observera behöver du sätta upp spårningen igen. Det här kan kännas lite ovant i början, men här är ett vanligt mönster:

func observeNetworkChanges(_ monitor: NetworkMonitor) {
    withObservationTracking {
        _ = monitor.isConnected
        _ = monitor.connectionType
    } onChange: {
        // Reagera på ändringen
        handleNetworkChange(monitor)

        // Återregistrera observeringen
        DispatchQueue.main.async {
            observeNetworkChanges(monitor)
        }
    }
}

Användning med UIKit

I UIKit-baserade projekt kan Observation-ramverket vara ett kraftfullt alternativ till KVO eller NotificationCenter:

class ProfileViewController: UIViewController {
    private let profile: UserProfile
    private let nameLabel = UILabel()
    private let emailLabel = UILabel()

    init(profile: UserProfile) {
        self.profile = profile
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        startObserving()
    }

    private func startObserving() {
        withObservationTracking {
            nameLabel.text = profile.name
            emailLabel.text = profile.email
        } onChange: { [weak self] in
            DispatchQueue.main.async {
                self?.startObserving()
            }
        }
    }

    private func setupUI() {
        // Konfigurera etiketternas layout...
        let stack = UIStackView(arrangedSubviews: [nameLabel, emailLabel])
        stack.axis = .vertical
        stack.spacing = 8
        view.addSubview(stack)
        stack.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stack.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

Det här mönstret är särskilt värdefullt i appar som gradvis migrerar från UIKit till SwiftUI — samma datamodell kan användas i båda kontexterna utan några ändringar.

Observation och Swift Concurrency

En viktig aspekt att förstå är hur Observation-ramverket interagerar med Swifts concurrency-modell, särskilt i Swift 6. @Observable-klasser är referenstyper och delas potentiellt mellan aktörer, vilket gör Sendable-kompatibilitet relevant.

@Observable med @MainActor

Det vanligaste mönstret för vy-modeller är att isolera dem till MainActor, eftersom de flesta UI-uppdateringar sker på huvudtråden:

@MainActor
@Observable
class ArticleViewModel {
    var articles: [Article] = []
    var isLoading: Bool = false
    var errorMessage: String?

    private let repository: ArticleRepository

    init(repository: ArticleRepository) {
        self.repository = repository
    }

    func loadArticles() async {
        isLoading = true
        errorMessage = nil

        do {
            articles = try await repository.fetchAll()
        } catch {
            errorMessage = "Kunde inte hämta artiklar: \(error.localizedDescription)"
        }

        isLoading = false
    }

    func deleteArticle(_ article: Article) async {
        do {
            try await repository.delete(article)
            articles.removeAll { $0.id == article.id }
        } catch {
            errorMessage = "Kunde inte radera artikeln."
        }
    }
}

Med Swift 6.2:s Approachable Concurrency och defaultIsolation(MainActor.self) kan du till och med skippa den explicita @MainActor-annoteringen — hela modulen isoleras till huvudtråden som standard. Det förenklar saker avsevärt.

Trådsäkerhet och @ObservationIgnored

Observation-ramverkets ObservationRegistrar är trådsäkert i sig, men dina egna egenskaper behöver fortfarande korrekt synkronisering om de nås från flera trådar. I praktiken innebär det att @MainActor-isolation är det säkraste valet för de flesta vy-modeller:

@MainActor
@Observable
class SafeCounter {
    var count: Int = 0

    // Tung beräkning som inte behöver köra på MainActor
    @concurrent
    nonisolated func computeNextValue() async -> Int {
        // Tungt arbete på bakgrundstråd
        try? await Task.sleep(for: .seconds(1))
        return Int.random(in: 1...100)
    }

    func updateCount() async {
        let newValue = await computeNextValue()
        // Tillbaka på MainActor, säkert att uppdatera
        count = newValue
    }
}

Avancerade mönster och arkitektur

Sammansatta modeller med nästlade @Observable

En av Observation-ramverkets riktiga styrkor är att det hanterar nästlade observerbara objekt naturligt. Inget extra arbete krävs:

@Observable
class Address {
    var street: String = ""
    var city: String = ""
    var postalCode: String = ""
}

@Observable
class Customer {
    var name: String = ""
    var address: Address = Address()
    var orders: [Order] = []
}

struct CustomerDetailView: View {
    @Bindable var customer: Customer

    var body: some View {
        Form {
            Section("Kundinformation") {
                TextField("Namn", text: $customer.name)
            }

            Section("Adress") {
                // Observation spårar automatiskt ändringen
                // genom customer.address.street
                AddressEditor(address: customer.address)
            }

            Section("Ordrar (\(customer.orders.count))") {
                ForEach(customer.orders) { order in
                    OrderRow(order: order)
                }
            }
        }
    }
}

struct AddressEditor: View {
    @Bindable var address: Address

    var body: some View {
        // Ritas bara om när just denna adress ändras
        TextField("Gata", text: $address.street)
        TextField("Stad", text: $address.city)
        TextField("Postnummer", text: $address.postalCode)
    }
}

Observation-ramverket spårar hela kedjan: om customer.address.city ändras ritas bara AddressEditor om — inte OrderRow-vyerna eller andra delar av formuläret. Det är precis så det borde fungera.

Repository-mönster med @Observable

Här är ett mer komplett arkitekturmönster som visar hur du kan strukturera en SwiftUI-app med @Observable. Det här är ett mönster jag använder i de flesta av mina egna projekt:

// Datamodell
struct Recipe: Identifiable, Codable {
    let id: UUID
    var title: String
    var ingredients: [String]
    var instructions: String
    var isFavorite: Bool
}

// Repository/Store
@Observable
class RecipeStore {
    var recipes: [Recipe] = []
    var isLoading: Bool = false

    private let apiClient: APIClient

    init(apiClient: APIClient = .shared) {
        self.apiClient = apiClient
    }

    var favoriteRecipes: [Recipe] {
        recipes.filter(\.isFavorite)
    }

    func load() async {
        isLoading = true
        defer { isLoading = false }

        do {
            recipes = try await apiClient.fetchRecipes()
        } catch {
            print("Fel vid hämtning: \(error)")
        }
    }

    func toggleFavorite(_ recipe: Recipe) {
        guard let index = recipes.firstIndex(where: { $0.id == recipe.id }) else { return }
        recipes[index].isFavorite.toggle()
    }
}

// Vy
struct RecipeListView: View {
    @Environment(RecipeStore.self) private var store

    var body: some View {
        NavigationStack {
            Group {
                if store.isLoading {
                    ProgressView("Laddar recept...")
                } else {
                    List(store.recipes) { recipe in
                        RecipeRow(recipe: recipe) {
                            store.toggleFavorite(recipe)
                        }
                    }
                }
            }
            .navigationTitle("Recept")
            .task { await store.load() }
        }
    }
}

struct RecipeRow: View {
    let recipe: Recipe
    let onToggleFavorite: () -> Void

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(recipe.title)
                    .font(.headline)
                Text("\(recipe.ingredients.count) ingredienser")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
            Spacer()
            Button(action: onToggleFavorite) {
                Image(systemName: recipe.isFavorite ? "heart.fill" : "heart")
                    .foregroundStyle(recipe.isFavorite ? .red : .gray)
            }
        }
    }
}

Migrera från ObservableObject till @Observable

Om du har ett befintligt projekt som använder ObservableObject kan migreringen göras stegvis. Det behöver inte vara allt-eller-inget. Här är en systematisk guide:

Steg 1: Uppdatera modellklasserna

// Före
class SettingsManager: ObservableObject {
    @Published var isDarkMode: Bool = false
    @Published var fontSize: CGFloat = 16
    @Published var username: String = ""
}

// Efter
@Observable
class SettingsManager {
    var isDarkMode: Bool = false
    var fontSize: CGFloat = 16
    var username: String = ""
}

Steg 2: Uppdatera property wrappers i vyer

// Före
struct SettingsView: View {
    @StateObject private var settings = SettingsManager()
    // eller
    @ObservedObject var settings: SettingsManager
    // eller
    @EnvironmentObject var settings: SettingsManager
}

// Efter
struct SettingsView: View {
    @State private var settings = SettingsManager()
    // eller (inget property wrapper behövs för passerade objekt!)
    var settings: SettingsManager
    // eller
    @Environment(SettingsManager.self) private var settings
}

Steg 3: Uppdatera bindningar

// Före — bindning fungerade automatiskt med @ObservedObject
struct EditorView: View {
    @ObservedObject var settings: SettingsManager

    var body: some View {
        Toggle("Mörkt läge", isOn: $settings.isDarkMode)
    }
}

// Efter — använd @Bindable
struct EditorView: View {
    @Bindable var settings: SettingsManager

    var body: some View {
        Toggle("Mörkt läge", isOn: $settings.isDarkMode)
    }
}

Steg 4: Uppdatera miljöinjektioner

// Före
ContentView()
    .environmentObject(settings)

// Efter
ContentView()
    .environment(settings)

Fallgropar vid migrering

Var uppmärksam på dessa vanliga problem vid migrering:

  • Objektinitiering i @State: Tung initiering i init() kan nu köras flera gånger (även om objektet kastas bort). Flytta tung logik till .task-modifieraren.
  • Combine-pipelines: Om din ObservableObject använde Combine-pipelines med $published-publishers behöver dessa ersättas. Använd onChange(of:)-modifieraren i vyer eller withObservationTracking i modeller.
  • Saknad @Bindable: Att glömma @Bindable när bindningar behövs ger kompilatorfel. Lägg till den där $-syntax används.
  • iOS 17-krav: @Observable kräver iOS 17+. Om du fortfarande stöder äldre versioner behöver du villkorlig kompilering eller en längre migreringsperiod.

Observation och SwiftData

SwiftData, Apples moderna ramverk för datapersistens, bygger direkt på Observation-ramverket. Alla @Model-klasser är automatiskt @Observable, vilket ger sömlös integration med SwiftUI:

@Model
class Note {
    var title: String
    var content: String
    var createdAt: Date
    var tags: [Tag]

    init(title: String, content: String, createdAt: Date = .now) {
        self.title = title
        self.content = content
        self.createdAt = createdAt
        self.tags = []
    }
}

struct NoteEditorView: View {
    @Bindable var note: Note

    var body: some View {
        Form {
            TextField("Titel", text: $note.title)
            TextEditor(text: $note.content)
        }
        .navigationTitle("Redigera anteckning")
    }
}

Eftersom @Model redan inkluderar @Observable-funktionalitet behöver du inte lägga till makrot separat. Ändringarna spåras automatiskt av både SwiftData (för persistens) och Observation (för vyuppdateringar).

Bästa praxis och rekommendationer

Baserat på community-erfarenheter och Apples egna riktlinjer — här är de viktigaste rekommendationerna för att arbeta med Observation-ramverket.

1. Använd @Observable som standard för alla modellklasser

Sluta använda ObservableObject i ny kod. @Observable är enklare, snabbare och framtidssäkert. Det finns helt enkelt ingen anledning att använda det gamla systemet om du stöder iOS 17+.

2. Isolera vy-modeller till @MainActor

Vy-modeller som driver UI bör isoleras till @MainActor. Detta säkerställer att alla egenskapsändringar sker på huvudtråden, vilket SwiftUI kräver för vyuppdateringar.

3. Undvik onödig observation

Använd @ObservationIgnored för egenskaper som inte påverkar UI: cachar, loggfiler, interna timers och liknande. Varje spårad egenskap har en viss overhead — om den aldrig läses av en vy finns det ingen anledning att spåra den.

4. Håll @Observable-klasser fokuserade

Undvik "god object"-antimönstret där en enda klass innehåller all appdata. Dela upp i mindre, fokuserade klasser som var och en hanterar ett specifikt domänområde. Ditt framtida jag kommer att tacka dig.

5. Använd struct för data, klass för beteende

Kombinera @Observable-klasser med struct-baserade datamodeller. Klassen hanterar affärslogik och tillståndshantering, medan struct-typerna representerar ren data:

// Datamodell som struct
struct Article: Identifiable, Codable {
    let id: UUID
    var title: String
    var body: String
}

// Tillståndshantering som @Observable klass
@Observable
class ArticleStore {
    var articles: [Article] = []

    func update(_ article: Article) {
        guard let index = articles.firstIndex(where: { $0.id == article.id }) else { return }
        articles[index] = article
    }
}

Sammanfattning och framåtblick

Observation-ramverket och @Observable-makrot representerar en av de viktigaste förbättringarna i SwiftUI:s historia. Genom att gå från push-baserad till pull-baserad observation med åtkomstspårning har Apple löst ett fundamentalt prestandaproblem, samtidigt som API:et blivit enklare och mer intuitivt.

Kort sagt ger @Observable dig:

  • Bättre prestanda — vyer ritas bara om när de egenskaper de faktiskt läser ändras
  • Mindre boilerplate — inga @Published, @StateObject eller @EnvironmentObject
  • Enklare mental modell — tre property wrappers istället för sex
  • Sömlös integration — med SwiftData, Swift Concurrency och framtida Apple-ramverk
  • Flexibilitet — fungerar utanför SwiftUI med withObservationTracking

Med iOS 26 och Swift 6.2 har ramverket mognat ytterligare, med bättre integration med den nya concurrency-modellen och förbättrad kompilatordiagnostik. Om du inte redan har börjat migrera till @Observable är det hög tid att göra det — det är inte bara framtiden, det är nuet.

Oavsett om du bygger en helt ny app eller gradvis migrerar en befintlig kodbas erbjuder Observation-ramverket en tydlig väg framåt mot enklare, snabbare och mer underhållbar SwiftUI-kod. Med de mönster och tekniker vi gått igenom i den här guiden har du verktygen du behöver för att ta steget fullt ut.

Om Författaren Editorial Team

Our team of expert writers and editors.