← All writing

SwiftUI @State and @Observable After WWDC 2026: The Initialization Problem Is Solved

WWDC 2026 fixed the subtle initialization difference between @State and @StateObject for Observable types, backported to iOS 17, and added native @Observable support in UIKit. Here is what changed and why it simplifies modern SwiftUI data flow.


There is a class of SwiftUI bug that is hard to describe in a code review but obvious in production: a view model that re-initializes when it should not, losing state that the user expects to persist. The root cause, for anyone who has chased it, is the difference between @State and @StateObject when a view is destroyed and recreated by SwiftUI’s structural updates.

WWDC 2026 resolved this. The @State property wrapper now handles @Observable types with the same initialization guarantees that @StateObject provided for ObservableObject types. The change is backported to iOS 17, which means the fix is available to the majority of active users on devices running current software.

This is not a cosmetic API change. It removes an entire category of subtle, timing-dependent state bug from SwiftUI code that uses the Observation framework.

The problem that existed

With iOS 17’s @Observable macro, the expected migration was to replace @StateObject with @State:

// iOS 16 and earlier pattern
@StateObject private var viewModel = OrderViewModel()

// iOS 17+ Observation macro pattern
@State private var viewModel = OrderViewModel()

The practical difference between them was not immediately obvious from the documentation, but it mattered in real applications. @StateObject guaranteed that the wrapped object was created exactly once per view instance, regardless of how many times SwiftUI re-evaluated the view’s initializer. @State applied to an @Observable class did not have this guarantee in iOS 17’s initial implementation. If a parent view caused SwiftUI to re-create the child view’s initializer while the child was still visible, @State could re-initialize the model, discarding in-progress state.

The result was views that reset their loading state, dismissed in-progress forms, or lost unsaved user input during navigation transitions or parent state updates.

What WWDC 2026 changed

The WWDC 2026 Observation framework updates aligned @State behaviour for @Observable types with the guarantees that @StateObject always provided. The @State attribute now acts as a macro for class-typed @Observable properties, ensuring initialization happens exactly once per view identity, not per evaluator pass.

The backport to iOS 17 means that code targeting iOS 17 and above can rely on this behaviour without conditional compilation.

import Observation
import SwiftUI

@Observable
final class OrderDetailViewModel {
    var order: Order?
    var isLoading = false
    var error: String?

    func load(orderId: String) async {
        isLoading = true
        error = nil
        do {
            order = try await OrderService.shared.fetchOrder(id: orderId)
        } catch {
            self.error = error.localizedDescription
        }
        isLoading = false
    }
}

struct OrderDetailView: View {
    let orderId: String

    // This is now safe: initialized exactly once per view instance
    @State private var viewModel = OrderDetailViewModel()

    var body: some View {
        Group {
            if viewModel.isLoading {
                ProgressView()
            } else if let order = viewModel.order {
                OrderContent(order: order)
            } else if let error = viewModel.error {
                ErrorView(message: error)
            }
        }
        .task {
            await viewModel.load(orderId: orderId)
        }
    }
}

No @StateObject. No ObservableObject. No @Published. The view model is a plain Swift class annotated with @Observable, and the property wrapper is @State.

UIKit and AppKit get native @Observable support

The other significant change from WWDC 2026 is native @Observable support in UIKit and AppKit. Previously, using @Observable in a UIKit view controller required manual observation with withObservationTracking(_:onChange:), which is an ergonomic step backward from Combine’s sink.

With the 2026 updates, UIKit view controllers can observe @Observable model changes directly:

import UIKit
import Observation

@Observable
final class ProfileViewModel {
    var displayName: String = ""
    var avatarURL: URL?
    var isLoading = false
}

final class ProfileViewController: UIViewController {
    private let viewModel = ProfileViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Native @Observable support in UIKit
        observe { [weak self] in
            guard let self else { return }
            self.nameLabel.text = self.viewModel.displayName
            self.loadingIndicator.isHidden = !self.viewModel.isLoading
        }
    }
}

The observe block re-runs automatically when any @Observable property accessed inside it changes. This is the same property-level dependency tracking that SwiftUI uses internally, exposed to UIKit without requiring Combine subscriptions or manual willSet/didSet boilerplate.

For mixed codebases with both SwiftUI and UIKit, this eliminates the pattern of maintaining two separate observation setups for the same model — one Combine-based for UIKit and one @Observable for SwiftUI.

The nonisolated keyword on extensions

Swift 6.1 and 6.2 added the ability to apply nonisolated to entire types and extensions, which reduces annotation noise in @Observable view models that have helpers or utilities that do not need main actor isolation:

@Observable
@MainActor
final class SearchViewModel {
    var query: String = ""
    var results: [SearchResult] = []

    func search() async {
        // Runs on @MainActor
        results = try? await SearchService.shared.search(query: query) ?? []
    }
}

// Utility methods that don't touch main-actor-isolated state
nonisolated extension SearchViewModel {
    func sanitisedQuery() -> String {
        query.trimmingCharacters(in: .whitespaces).lowercased()
    }

    func isQueryValid() -> Bool {
        sanitisedQuery().count >= 2
    }
}

Without nonisolated on the extension, each method would require its own nonisolated annotation. The extension-level keyword applies to all members at once.

Practical upgrade checklist

For projects currently on iOS 17 using @Observable:

[ ] Xcode 17+ with the iOS 17 SDK base target
[ ] @StateObject replaced with @State for all @Observable properties
[ ] @ObservableObject conformances replaced with @Observable macro where appropriate
[ ] @Published property wrappers removed from @Observable classes
[ ] UIKit observation migrated from withObservationTracking to observe() where available
[ ] Concurrency diagnostics reviewed: @Observable classes on @MainActor are the recommended model
[ ] Unit tests cover view model initialization to verify one-time-init guarantee

The transition is incremental. ObservableObject continues to work. The migration is a modernisation toward a simpler model, not a required breaking change. New code should use @Observable; existing ObservableObject code does not need immediate replacement.

The initialization guarantee being solved is the most material change for production apps. Teams that avoided @State for class types because of the re-initialization risk can now use it with confidence.