Article by Pushkar Deshmukh

SwiftUI Observation in iOS 17+: Thread-Safe @Observable ViewModels for SwiftUI and UIKit

iOS 17 introduced Swift Observation, replacing ObservableObject and @Published. Learn how to build a thread-safe @Observable ViewModel, use it in SwiftUI without property wrappers, and integrate the same state layer with UIKit.

Pushkar Deshmukh

Pushkar Deshmukh

Senior iOS Engineer

March 8, 20262 min read12 views
SwiftUIiOS DevelopmentSwift Observation@ObservableUIKitSwift ConcurrencyiOS 17Swift ArchitectureThread SafetyMobile Engineering
SwiftUI Observation in iOS 17+: Thread-Safe @Observable ViewModels for SwiftUI and UIKit

Swift Observation: The Modern Way to Manage UI State

Starting in iOS 17, Apple introduced the Observation framework with the @Observable macro.

It replaces the old Combine-based pattern:

  • ObservableObject

  • @Published

  • @ObservedObject

  • @StateObject

With Observation, Swift automatically tracks which properties your UI reads and updates the view when those values change.

But there is an important architectural rule:

@Observable tracks changes, but it does not guarantee thread safety.

For UI state, the safest pattern is to isolate your model using @MainActor.

Let's see a minimal counter example that works in both SwiftUI and UIKit.


Step 1 — Thread-Safe ViewModel

import Observation

@MainActor
@Observable
class CounterViewModel {

    var count = 0

    func increment() {
        count += 1
    }
}

Why @MainActor?

UI state must be mutated on the main thread.

@MainActor ensures:

  • Safe state mutation

  • No data races

  • Compile-time enforcement by Swift


SwiftUI Example (No Wrapper Needed)

With Observation, SwiftUI can track an observable model even without property wrappers, as long as the object is injected into the view.

import SwiftUI

struct CounterView: View {

    let viewModel: CounterViewModel

    var body: some View {
        VStack(spacing: 20) {

            Text("Count: \(viewModel.count)")
                .font(.largeTitle)

            Button("Increment") {
                viewModel.increment()
            }
        }
        .padding()
    }
}

SwiftUI automatically tracks:

viewModel.count

Whenever count changes, the view refreshes.

No @ObservedObject.

No @StateObject.

No Combine.


Creating the ViewModel

The parent view owns the model and injects it:

struct RootView: View {

    @State private var viewModel = CounterViewModel()

    var body: some View {
        CounterView(viewModel: viewModel)
    }
}

Use @State here because the view owns the instance and must preserve it across re-renders.


UIKit Example

UIKit does not automatically track dependencies, so we use withObservationTracking.

import UIKit
import Observation

class CounterViewController: UIViewController {

    private let viewModel = CounterViewModel()

    private let label = UILabel()
    private let button = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()

        setupUI()
        observe()
    }

    private func observe() {

        withObservationTracking {

            label.text = "Count: \(viewModel.count)"

        } onChange: { [weak self] in

            Task { @MainActor in
                self?.observe()
            }
        }
    }

    @objc
    private func buttonTapped() {
        viewModel.increment()
    }
}

UIKit requires manual observation, but the same ViewModel works without modification.


SwiftUI vs UIKit with Observation

Feature

SwiftUI

UIKit

Dependency tracking

Automatic

Manual

State updates

Automatic

Manual

Combine required

No

No

Thread safety

@MainActor ViewModel

@MainActor ViewModel


Key Takeaways

Modern iOS state management is much simpler:

  • Use @Observable instead of ObservableObject

  • Use @MainActor for thread-safe UI state

  • Inject models directly into SwiftUI views

  • Use @State only when the view owns the instance

The result is cleaner architecture, less boilerplate, and safer UI state updates across SwiftUI and UIKit.

Pushkar Deshmukh

Written by

Pushkar Deshmukh

Senior iOS Engineer

11+ years of experience building mobile and web applications. Passionate about Swift, React, and sharing knowledge through technical writing.

12views0comments

Comments

Loading…

Loading comments…

Read next

View all
Back to All Articles