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
ObservableObjectanvände Combine-pipelines med$published-publishers behöver dessa ersättas. AnvändonChange(of:)-modifieraren i vyer ellerwithObservationTrackingi modeller. - Saknad @Bindable: Att glömma
@Bindablenär bindningar behövs ger kompilatorfel. Lägg till den där$-syntax används. - iOS 17-krav:
@Observablekrä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,@StateObjecteller@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.