SwiftUI @Observable vs @State: Wanneer Wat Gebruiken in 2026

Beheers de verschillen tussen @Observable en @State in SwiftUI om de juiste tool voor state management in iOS-apps te kiezen.

Vergelijking van @Observable en @State in SwiftUI voor iOS-developers

State management vormt de hoeksteen van elke performante SwiftUI-applicatie. Sinds iOS 17 heeft de @Observable-macro de creatie van reactieve modellen gerevolutioneerd, terwijl @State essentieel blijft voor lokale view-state. Begrijpen wanneer welke tool gebruikt moet worden, voorkomt onnodige re-renders en maakt soepele, responsieve apps mogelijk.

Wat dit artikel behandelt

Dit artikel verkent de interne mechanismen van @Observable en @State, hun fundamentele verschillen en biedt duidelijke richtlijnen om de juiste tool te kiezen op basis van de context. Inclusief de iOS 27 @State-macro wijzigingen (teruggeport naar iOS 17).

Fundamenten van @State

@State vertegenwoordigt de eenvoudigste vorm van state management in SwiftUI. Deze property wrapper creëert persistente opslag voor een waarde die uitsluitend toebehoort aan de view die hem declareert.

CounterView.swiftswift
struct CounterView: View {
    // @State creates storage managed by SwiftUI
    @State private var count = 0

    var body: some View {
        VStack(spacing: 20) {
            // The view updates when count changes
            Text("Counter: (count)")
                .font(.largeTitle)

            HStack(spacing: 16) {
                Button("- 1") {
                    count -= 1
                }

                Button("+ 1") {
                    count += 1
                }
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

Elke wijziging aan count activeert een re-render van de view. SwiftUI beheert automatisch de levenscyclus van deze waarde en behoudt hem tussen body-reconstructies.

Belangrijkste kenmerken van @State

@State heeft verschillende onderscheidende eigenschappen die het optimale gebruik bepalen:

StateCharacteristics.swiftswift
struct FormView: View {
    // Simple local state with value types
    @State private var username = ""
    @State private var isEnabled = true
    @State private var selectedIndex = 0

    // Complex value types supported
    @State private var configuration = FormConfiguration()

    var body: some View {
        Form {
            TextField("Username", text: $username)

            Toggle("Enabled", isOn: $isEnabled)

            Picker("Option", selection: $selectedIndex) {
                Text("Option A").tag(0)
                Text("Option B").tag(1)
                Text("Option C").tag(2)
            }
        }
    }
}

// Structs work perfectly with @State
struct FormConfiguration: Equatable {
    var theme: Theme = .light
    var fontSize: CGFloat = 16
    var showNotifications: Bool = true
}

enum Theme {
    case light, dark, system
}

Het cruciale punt: @State werkt met value types (structs, enums, primitieve types). Voor reference types (klassen) zijn andere tools nodig.

De @Observable-macro uitgelegd

Geïntroduceerd met iOS 17, transformeert @Observable elke klasse in een reactieve databron. In tegenstelling tot het oudere ObservableObject-protocol biedt deze macro fijnmazige observatie: alleen properties die daadwerkelijk door een view worden gelezen, activeren de re-render. De Observation framework documentatie biedt de volledige API-referentie.

UserModel.swiftswift
import Observation

// @Observable transforms the class into a reactive source
@Observable
class UserModel {
    var name: String = ""
    var email: String = ""
    var avatarURL: URL?
    var preferences = UserPreferences()

    // Computed properties work too
    var isValid: Bool {
        !name.isEmpty && email.contains("@")
    }
}

struct UserPreferences {
    var newsletter: Bool = false
    var notifications: Bool = true
    var theme: Theme = .system
}

De magie gebeurt op compile-time: de macro genereert automatisch de noodzakelijke tracking-code voor elke property.

Granulaire observatie in actie

Het grote verschil met het oude ObservableObject ligt in de tracking-granulariteit:

GranularObservation.swiftswift
@Observable
class ProfileModel {
    var name: String = ""
    var bio: String = ""
    var followerCount: Int = 0
    var posts: [Post] = []
}

struct ProfileHeaderView: View {
    let model: ProfileModel

    var body: some View {
        VStack {
            // This view only re-renders if name or bio change
            Text(model.name)
                .font(.title)
            Text(model.bio)
                .foregroundStyle(.secondary)
        }
    }
}

struct FollowerCountView: View {
    let model: ProfileModel

    var body: some View {
        // This view only re-renders if followerCount changes
        HStack {
            Image(systemName: "person.2")
            Text("(model.followerCount) followers")
        }
    }
}

struct ProfileScreen: View {
    @State private var model = ProfileModel()

    var body: some View {
        VStack {
            // Each subview tracks only its dependencies
            ProfileHeaderView(model: model)
            FollowerCountView(model: model)

            Button("Simulate new follower") {
                // Only re-renders FollowerCountView
                model.followerCount += 1
            }
        }
    }
}
Automatische tracking

SwiftUI analyseert de body van elke view om te bepalen welke properties worden gelezen. Alleen die properties activeren een re-render bij wijziging.

Directe vergelijking: @Observable vs @State

De keuze tussen deze tools hangt af van verschillende factoren. Hier een gestructureerde vergelijking:

ComparisonExample.swiftswift
// Scenario 1: Temporary UI state -> @State
struct ToggleExample: View {
    @State private var isExpanded = false  // @State appropriate

    var body: some View {
        VStack {
            Button(isExpanded ? "Collapse" : "Expand") {
                withAnimation {
                    isExpanded.toggle()
                }
            }

            if isExpanded {
                Text("Detailed content...")
            }
        }
    }
}

// Scenario 2: Shared business data -> @Observable
@Observable
class CartModel {  // @Observable appropriate
    var items: [CartItem] = []
    var promoCode: String?

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

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

    func addItem(_ item: CartItem) {
        if let index = items.firstIndex(where: { $0.id == item.id }) {
            items[index].quantity += 1
        } else {
            items.append(item)
        }
    }

    func removeItem(_ item: CartItem) {
        items.removeAll { $0.id == item.id }
    }
}

struct CartItem: Identifiable, Equatable {
    let id: UUID
    let name: String
    let price: Decimal
    var quantity: Int
}

Use case-overzichtstabel

Criterium@State@Observable
DatatypeValue types (struct, enum)Klassen
ScopeLokaal in een viewDeelbaar tussen views
ComplexiteitSimpele stateComplexe businesslogica
LevenscyclusBeheerd door SwiftUIExpliciet beheerd
Re-renderVolledige viewGranulair per property

Klaar om je iOS gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

iOS 27 update: @State als macro met lazy initialisatie

WWDC 2026 introduceerde een significante wijziging aan @State: het wordt nu geïmplementeerd als macro in plaats van als property wrapper. Deze wijziging adresseert direct een veelvoorkomend probleem bij het gebruik van @State met @Observable-klassen.

Het probleem voor iOS 27

Voorheen werden @Observable-klassen opgeslagen in @State eager geïnitialiseerd. Dit kon ertoe leiden dat dezelfde instantie meerdere keren werd aangemaakt voordat de view verscheen:

BeforeIOS27.swiftswift
// Before iOS 27: potential multiple initializations
struct StickerStoreView: View {
    // The StickerStore instance could be created eagerly
    // and recreated during parent view updates
    @State private var store = StickerStore()

    var body: some View {
        // ...
    }
}

@Observable
class StickerStore {
    init() {
        print("StickerStore initialized")
        // This could print multiple times before the view appeared
    }
}

De oplossing: lazy initialisatie

Met iOS 27 (en teruggeport naar iOS 17) worden @Observable-klassen opgeslagen in @State nu lazy geïnitialiseerd, precies één keer voor de levensduur van de view:

AfterIOS27.swiftswift
// iOS 27+: guaranteed single initialization
struct StickerStoreView: View {
    @State private var store = StickerStore()

    var body: some View {
        // store is created exactly once, when the view first needs it
        StoreContentView(store: store)
    }
}

@Observable
class StickerStore {
    var stickers: [Sticker] = []

    init() {
        print("StickerStore initialized")
        // Now prints exactly once per view lifetime
    }
}
Wijziging in initializer-patroon

Bij het toewijzen van @State in een initializer moet de standaardwaarde worden verwijderd om compilerfouten te voorkomen:

InitializerPattern.swiftswift
// Correct pattern when using custom initializer
struct StickerPageView: View {
    @State private var page: StickerPage  // No default value
    let title: String

    init(title: String) {
        self.page = StickerPage(title: title)
        self.title = title
    }

    var body: some View {
        // ...
    }
}

Deze fix elimineert verspillende objectallocaties en zorgt ervoor dat @Observable-instanties correct blijven bestaan gedurende de view-levenscyclus. Voor ontwikkelaars die zich voorbereiden op iOS-sollicitatievragen, demonstreert begrip van deze wijziging kennis van actuele SwiftUI-internals.

Geavanceerde gebruikspatronen

@State en @Observable combineren

In echte applicaties bestaan deze tools harmonieus naast elkaar. @State beheert lokale UI-state terwijl @Observable businessdata inkapselt.

CombinedPatterns.swiftswift
@Observable
class TodoListModel {
    var todos: [Todo] = []
    var filter: TodoFilter = .all

    var filteredTodos: [Todo] {
        switch filter {
        case .all:
            return todos
        case .active:
            return todos.filter { !$0.isCompleted }
        case .completed:
            return todos.filter { $0.isCompleted }
        }
    }

    func addTodo(title: String) {
        let todo = Todo(id: UUID(), title: title, isCompleted: false)
        todos.append(todo)
    }

    func toggleTodo(_ todo: Todo) {
        guard let index = todos.firstIndex(where: { $0.id == todo.id }) else { return }
        todos[index].isCompleted.toggle()
    }
}

struct Todo: Identifiable, Equatable {
    let id: UUID
    var title: String
    var isCompleted: Bool
}

enum TodoFilter: CaseIterable {
    case all, active, completed
}

struct TodoListView: View {
    // Business data via @Observable (lazy init since iOS 27)
    @State private var model = TodoListModel()

    // Local UI state via @State
    @State private var newTodoTitle = ""
    @State private var isAddingTodo = false
    @State private var selectedTodo: Todo?

    var body: some View {
        NavigationStack {
            VStack {
                // Filter with Picker
                Picker("Filter", selection: $model.filter) {
                    ForEach(TodoFilter.allCases, id: .self) { filter in
                        Text(filter.label).tag(filter)
                    }
                }
                .pickerStyle(.segmented)
                .padding()

                // Todo list
                List(model.filteredTodos, selection: $selectedTodo) { todo in
                    TodoRowView(todo: todo) {
                        model.toggleTodo(todo)
                    }
                }
            }
            .navigationTitle("Tasks")
            .toolbar {
                Button {
                    isAddingTodo = true
                } label: {
                    Image(systemName: "plus")
                }
            }
            .sheet(isPresented: $isAddingTodo) {
                AddTodoSheet(model: model)
            }
        }
    }
}

struct TodoRowView: View {
    let todo: Todo
    let onToggle: () -> Void

    var body: some View {
        HStack {
            Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                .foregroundStyle(todo.isCompleted ? .green : .secondary)
                .onTapGesture(perform: onToggle)

            Text(todo.title)
                .strikethrough(todo.isCompleted)
        }
    }
}

extension TodoFilter {
    var label: String {
        switch self {
        case .all: return "All"
        case .active: return "Active"
        case .completed: return "Completed"
        }
    }
}

@Observable met dependency injection

Voor complexere applicaties maakt injectie via de SwiftUI-environment effectieve ontkoppeling mogelijk:

DependencyInjection.swiftswift
@Observable
class AuthenticationService {
    var currentUser: User?
    var isAuthenticated: Bool { currentUser != nil }

    func login(email: String, password: String) async throws {
        // Authentication logic
        currentUser = User(id: UUID(), email: email, name: "User")
    }

    func logout() {
        currentUser = nil
    }
}

struct User: Identifiable, Equatable {
    let id: UUID
    let email: String
    let name: String
}

// Extension to create an environment key
extension EnvironmentValues {
    @Entry var authService: AuthenticationService = AuthenticationService()
}

// Configuration in the App
@main
struct MyApp: App {
    @State private var authService = AuthenticationService()

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

// Usage in views
struct ProfileView: View {
    @Environment(.authService) private var authService

    var body: some View {
        if let user = authService.currentUser {
            VStack {
                Text("Hello, (user.name)")
                Button("Sign Out") {
                    authService.logout()
                }
            }
        } else {
            Text("Not signed in")
        }
    }
}

Performance en optimalisatie

Onnodige re-renders vermijden

Zelfs met de granulariteit van @Observable kunnen bepaalde patronen de performance verslechteren:

PerformanceOptimization.swiftswift
// Bad pattern: reading the entire object
struct BadPatternView: View {
    let model: ProfileModel

    var body: some View {
        // Reads model.name AND model.posts even if only name is displayed
        let _ = model.posts.count  // Creates unnecessary dependency
        Text(model.name)
    }
}

// Good pattern: targeted reading
struct GoodPatternView: View {
    let model: ProfileModel

    var body: some View {
        // Tracks only name
        Text(model.name)
    }
}

// Extract into subviews to isolate dependencies
struct OptimizedProfileView: View {
    let model: ProfileModel

    var body: some View {
        VStack {
            // Each subview has its own dependencies
            ProfileNameView(model: model)
            ProfilePostsView(model: model)
            ProfileStatsView(model: model)
        }
    }
}

struct ProfileNameView: View {
    let model: ProfileModel

    var body: some View {
        Text(model.name)
            .font(.title)
    }
}

struct ProfilePostsView: View {
    let model: ProfileModel

    var body: some View {
        ForEach(model.posts) { post in
            PostRow(post: post)
        }
    }
}

struct ProfileStatsView: View {
    let model: ProfileModel

    var body: some View {
        HStack {
            StatBadge(value: model.followerCount, label: "Followers")
            StatBadge(value: model.posts.count, label: "Posts")
        }
    }
}
Dure computed properties

Computed properties op @Observable worden bij elke toegang opnieuw geëvalueerd. Voor complexe berekeningen is het verstandig het resultaat te cachen in een stored property.

Geavanceerde observation met Swift 6.4

Swift 6.4 introduceerde SE-0506, met fijnmazige en continue change notification API's voor het Observation framework. Deze geavanceerde tools richten zich op middleware-ontwikkelaars en widget-systeem implementaties, niet op typische app-ontwikkeling. De meeste applicaties blijven het standaard @Observable macro-patroon gebruiken. De Swift 6.4 release notes beschrijven deze toevoegingen in detail.

Migreren vanuit ObservableObject

Voor bestaande projecten met ObservableObject vereenvoudigt migratie naar @Observable de code:

MigrationExample.swiftswift
// Old pattern with ObservableObject
class OldSettingsModel: ObservableObject {
    @Published var darkMode: Bool = false
    @Published var fontSize: CGFloat = 16
    @Published var notifications: Bool = true
}

struct OldSettingsView: View {
    @StateObject private var settings = OldSettingsModel()
    // or @ObservedObject if injected

    var body: some View {
        Form {
            Toggle("Dark Mode", isOn: $settings.darkMode)
            Slider(value: $settings.fontSize, in: 12...24)
            Toggle("Notifications", isOn: $settings.notifications)
        }
    }
}

// New pattern with @Observable
@Observable
class NewSettingsModel {
    var darkMode: Bool = false
    var fontSize: CGFloat = 16
    var notifications: Bool = true
}

struct NewSettingsView: View {
    @State private var settings = NewSettingsModel()

    var body: some View {
        Form {
            Toggle("Dark Mode", isOn: $settings.darkMode)
            Slider(value: $settings.fontSize, in: 12...24)
            Toggle("Notifications", isOn: $settings.notifications)
        }
    }
}

Voordelen van migratie:

  • Geen @Published meer nodig op elke property
  • @State vervangt @StateObject voor creatie (met gegarandeerde lazy initialisatie)
  • Automatische granulaire observatie
  • Beter leesbare en onderhoudbare code

Voor apps die iOS 17+ targeten, wordt migratie naar @Observable aanbevolen. De Combine vs async/await migratiegids behandelt gerelateerde modernisatiepatronen.

Praktische beslissingsregels

Hier een beslissingsgids om de juiste tool te kiezen:

DecisionGuide.swiftswift
/*
 RULE 1: Ephemeral UI state -> @State
 - Animations, transitions
 - Local form states
 - Temporary selections
 - Section expand/collapse
*/
struct AnimatedCard: View {
    @State private var isFlipped = false  // Local UI state
    // ...
}

/*
 RULE 2: Shared data across views -> @Observable
 - Business data models
 - Authentication state
 - Shopping cart
 - User preferences
*/
@Observable
class UserSession {  // Shared across app
    var user: User?
    var preferences: Preferences
    // ...
}

/*
 RULE 3: Simple struct with binding -> @State
 - Local configuration
 - Isolated forms
*/
struct FormData {
    var name: String = ""
    var email: String = ""
}

struct FormView: View {
    @State private var formData = FormData()  // Struct with @State
    // ...
}

/*
 RULE 4: Complex business logic -> @Observable
 - Validations
 - Network calls
 - Data transformations
*/
@Observable
class OrderProcessor {  // Complex logic
    var items: [OrderItem] = []
    var status: OrderStatus = .draft

    func validate() -> [ValidationError] { /* ... */ }
    func submit() async throws { /* ... */ }
}

Sources

Kiezen tussen @Observable en @State in SwiftUI

De keuze tussen @Observable en @State komt neer op twee fundamentele vragen: het datatype (waarde of referentie) en de scope van de state (lokaal of gedeeld). @State blinkt uit voor simpele, lokale UI-states, terwijl @Observable schittert voor complexe datamodellen die granulaire observatie vereisen.

Beslissings-checklist

  • Gebruik @State voor value types en kortstondige UI-state
  • Gebruik @Observable voor klassen met businessdata
  • Geef de voorkeur aan @Observable wanneer state meerdere views beslaat
  • Extraheer in subviews om re-renders te optimaliseren
  • Vermijd het lezen van onnodige properties in de body
  • Migreer geleidelijk vanuit ObservableObject
  • Gebruik de environment voor dependency injection
  • Test performance met Instruments voor complexe gevallen

Met de iOS 27 @State-macro verbeteringen (teruggeport naar iOS 17) is het combineren van deze tools betrouwbaarder dan ooit. De lazy initialisatie fix verwijdert een veelvoorkomende bron van bugs bij het opslaan van @Observable-klassen in @State.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Dagelijkse challenge

Zie jij de bug in iOS?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter van SharpSkill

Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.

Bijgewerkt op 5 oktober 2026

Tags

#swiftui
#ios
#observable
#state-management
#swift

Delen

Gerelateerde artikelen