Automatic observation tracking in UIKit with @Observable

UIKit keeps getting better. iOS 26 adds native support for Swift Observation — read properties of an @Observable class inside a UIKit update method, and UIKit tracks those reads automatically, invalidating and re-rendering only what changed.

No manual withObservationTracking() wiring. No notification juggling. Just state and a view that stays in sync.

The backport goes all the way to iOS 18, which makes this practical to adopt today.

The setup

The example is a UIKit screen that reflects a user-selected app theme, with the theme picker itself built in SwiftUI and presented as a half-sheet. Changing the selection in SwiftUI should immediately update the UIKit view beneath it — and that’s exactly what automatic observation tracking handles for us.

@Observable
final class AppTheme {
    var accent: AccentColor = .forest

    enum AccentColor: String, CaseIterable {
        case forest, coral, sage
    }
}

final class ViewController: UIViewController {
    private let theme = AppTheme()

    private var headerView: UIView!
    private var themeLabel: UILabel!

    ...

    @objc private func showThemePicker() {
        let picker = ThemePickerView(theme: theme)
        let host = UIHostingController(rootView: picker)

        // half-sheet setup...

        present(host, animated: true)
    }
}

struct ThemePickerView: View {
    let theme: AppTheme

    var body: some View {
        NavigationStack {
            List(AppTheme.AccentColor.allCases, id: \.self) { color in
                Button {
                    theme.accent = color
                } label: {
                    Text(color.rawValue.capitalized)
                }
            }
            .navigationTitle("Theme")
        }
    }
}

iOS 26 — updateProperties()

iOS 26 introduces updateProperties(), a new lifecycle method that runs just before viewWillLayoutSubviews(). It’s now the recommended place to apply content, styling, and configuration — and it’s where automatic observation tracking is active.

override func updateProperties() {
    super.updateProperties()

    headerView.backgroundColor = UIColor(named: theme.accent.rawValue)
    themeLabel.text = theme.accent.rawValue.capitalized
}

Any @Observable property you read inside updateProperties() gets tracked. When the SwiftUI picker updates theme.accent, UIKit picks it up and calls the method again — no boilerplate, no observers to manage.

iOS 18 — viewWillLayoutSubviews()

To get the same behaviour on iOS 18, two things are needed.

First, add UIObservationTrackingEnabled to Info.plist and set it to YES.

Then, move the update logic into viewWillLayoutSubviews(), since updateProperties() is iOS 26-only:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    headerView.backgroundColor = UIColor(named: theme.accent.rawValue)
    themeLabel.text = theme.accent.rawValue.capitalized
}

UIKit tracks observation in viewWillLayoutSubviews() the same way — opt in via Info.plist, and the rest works identically.

Worth adopting

If you’re working in a mixed UIKit/SwiftUI codebase, this removes a whole class of boilerplate. Shared @Observable state just works across both frameworks, and the UIKit side updates exactly when it should without any manual subscription logic.