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.

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.
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.
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.
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:
struct FormView: View {
// ✅ Simple local state - 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.
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:
@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
}
}
}
}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:
// 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 | |-----------|--------|-------------| | Datatype | Value types (struct, enum) | Klassen | | Scope | Lokaal in een view | Deelbaar tussen views | | Complexiteit | Simpele state | Complexe businesslogica | | Levenscyclus | Beheerd door SwiftUI | Expliciet beheerd | | Re-render | Volledige view | Granulair per property |
Klaar om je iOS gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Geavanceerde gebruikspatronen
@State en @Observable combineren
In echte applicaties bestaan deze tools harmonieus naast elkaar. @State beheert lokale UI-state terwijl @Observable businessdata inkapselt.
@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
@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:
@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:
// ❌ 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")
}
}
}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.
Batch-updates met withObservationTracking
Voor geavanceerde scenario's maakt withObservationTracking het mogelijk wijzigingen te detecteren zonder een binding te creëren:
import Observation
@Observable
class DataSyncModel {
var lastSyncDate: Date?
var pendingChanges: Int = 0
var isSyncing: Bool = false
}
class SyncCoordinator {
let model: DataSyncModel
init(model: DataSyncModel) {
self.model = model
startObserving()
}
private func startObserving() {
// Observe changes without UI
withObservationTracking {
// Access that creates dependencies
_ = model.pendingChanges
_ = model.isSyncing
} onChange: {
// Called when an observed property changes
Task { @MainActor in
self.handleModelChange()
}
}
}
private func handleModelChange() {
if model.pendingChanges > 0 && !model.isSyncing {
// Trigger synchronization
Task {
await syncChanges()
}
}
// Re-establish observation
startObserving()
}
private func syncChanges() async {
model.isSyncing = true
// Sync logic...
model.isSyncing = false
model.pendingChanges = 0
model.lastSyncDate = Date()
}
}Migreren vanuit ObservableObject
Voor bestaande projecten met ObservableObject vereenvoudigt migratie naar @Observable de code:
// ❌ 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
@Publishedmeer nodig op elke property @Statevervangt@StateObjectvoor creatie- Automatische granulaire observatie
- Beter leesbare en onderhoudbare code
Praktische beslissingsregels
Hier een beslissingsgids om de juiste tool te kiezen:
/*
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 { /* ... */ }
}Conclusie
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
@Statevoor value types en kortstondige UI-state - ✅ Gebruik
@Observablevoor klassen met businessdata - ✅ Geef de voorkeur aan
@Observablewanneer 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
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Tags
Delen
Gerelateerde artikelen

SwiftUI Performance: LazyVStack en Complexe Lijsten Optimaliseren
Optimalisatietechnieken voor LazyVStack en SwiftUI-lijsten. Verminder geheugengebruik, verbeter scrollprestaties en vermijd veelvoorkomende valkuilen.

Custom SwiftUI ViewModifiers: herbruikbare patterns voor design systems
Bouw custom ViewModifiers in SwiftUI voor een consistent design system. Patterns, best practices en praktische voorbeelden om iOS-views efficiënt te stijlen.

SwiftUI: Moderne Interfaces Bouwen voor iOS
Een complete handleiding voor het bouwen van moderne gebruikersinterfaces met SwiftUI: declaratieve syntaxis, componenten, animaties en best practices voor iOS 18.