Swiftyn LogoSwiftyn
LearnInterview PrepRoadmapsArchitect Profile
Swift LanguageSwiftUIUIKitiOS ConceptsmacOS

SwiftUI Topics

Introduction to SwiftUISwiftUI App LifecycleViews and ModifiersTextImageShapesSF SymbolsButtonsStacksSpacerDividerScrollViewGroupsSectionsHStackVStackZStackLazyVStackLazyHStackLazyVGridLazyHGridGridGridRowGeometryReaderSafeAreaFrames and AlignmentPaddingOverlay and BackgroundPreferenceKeyAnchor PreferencesCustom Layouts@State@Binding@ObservedObject@StateObject@EnvironmentObject@Environment@PublishedObservableObject@Observable@BindableData Flow in SwiftUITwo Way BindingNavigationStackNavigationSplitViewNavigationPathProgrammatic NavigationTabViewDeep LinkingSheetFullScreenCoverPopoverAlertsCustom AnimationsAsync Await in SwiftUIUIKit Integration
Browse SwiftUI Topics
Introduction to SwiftUISwiftUI App LifecycleViews and ModifiersTextImageShapesSF SymbolsButtonsStacksSpacerDividerScrollViewGroupsSectionsHStackVStackZStackLazyVStackLazyHStackLazyVGridLazyHGridGridGridRowGeometryReaderSafeAreaFrames and AlignmentPaddingOverlay and BackgroundPreferenceKeyAnchor PreferencesCustom Layouts@State@Binding@ObservedObject@StateObject@EnvironmentObject@Environment@PublishedObservableObject@Observable@BindableData Flow in SwiftUITwo Way BindingNavigationStackNavigationSplitViewNavigationPathProgrammatic NavigationTabViewDeep LinkingSheetFullScreenCoverPopoverAlertsCustom AnimationsAsync Await in SwiftUIUIKit Integration
Swiftyn Logo

Swiftyn

The go-to platform for Apple developers. Swift, SwiftUI, and beyond.

Questions? Email us at support@swe180.com

Categories

  • SwiftUI
  • Swift Language
  • Xcode
  • visionOS

Our Products

  • SWE180
  • One Percent Engineer

Resources

  • About
  • RSS Feed
  • Apple Developer

© 2026 Swiftyn. All rights reserved.

Privacy PolicyTerms of Service

Swiftyn is the premier learning platform and developer resource for mastering the Apple ecosystem. Whether you are an aspiring iOS developer looking to learn Swift 6, a macOS engineer diving into advanced system architecture, or an XR pioneer building the future with visionOS, our beautifully crafted tutorials, roadmaps, and interview prep guides have you covered. Built by Apple developers, for Apple developers.

SwiftUI12 min read

Mastering @Observable: SwiftUI's Modern Data Observation

SwiftUI's data observation landscape has evolved significantly with the introduction of the @Observable macro. This modern approach offers improved performance, clearer syntax, and automatic dependency tracking, addressing many pain points of its predecessor, ObservableObject. Understanding and utilizing @Observable is crucial for building efficient and maintainable SwiftUI applications.

Introduction to SwiftUI's @Observable Macro

SwiftUI has always excelled at reactive UI development, automatically updating your interfaces when underlying data changes. Historically, ObservableObject and the @Published property wrapper were the go-to mechanisms for making custom data types observable. While effective, this approach had its limitations, particularly around performance and boilerplate code.

Enter the @Observable macro, introduced in iOS 17 and macOS 14. This powerful new tool simplifies how you declare observable models, leveraging Swift's macro capabilities to automatically synthesize the necessary observation mechanisms. Its core promise is to make your views update only when the data they actually depend on changes, leading to more efficient rendering and a better developer experience.

The @Observable macro fundamentally changes how SwiftUI tracks dependencies. Instead of relying on a publisher to send changes for every @Published property, @Observable uses a more granular, behind-the-scenes mechanism. When a property of an @Observable object is accessed within a SwiftUI View's body or init (or other observation scopes), SwiftUI automatically registers that property as a dependency. If that specific property then changes, only the views observing it will re-render.

Migrating from ObservableObject to @Observable

The migration process from ObservableObject to @Observable is surprisingly straightforward, often requiring minimal code changes. Let's look at a classic ObservableObject example and transform it.

First, consider a typical ObservableObject class:

ObservableObject requires you to inherit from the ObservableObject protocol and explicitly mark properties that trigger UI updates with @Published. When using such an object in a view, you'd typically use @StateObject for ownership and @ObservedObject for references, or @EnvironmentObject for shared global state. This pattern works but introduces boilerplate.

Now, let's migrate this to @Observable. The key steps are:

  1. Remove conformance to ObservableObject.
  2. Remove all @Published property wrappers.
  3. Add the @Observable macro to your class (or struct, if it's a reference type).

That's it! The @Observable macro handles the rest, synthesizing the necessary observation conformances and change notification mechanisms automatically. This results in cleaner, more concise model definitions.

swift
import SwiftUI

// Traditional ObservableObject 
class OldUserViewModel: ObservableObject {
    @Published var name: String = "John Doe"
    @Published var age: Int = 30
    @Published var email: String? = nil

    func updateName(_ newName: String) {
        name = newName
    }
}

struct OldUserView: View {
    @StateObject var viewModel = OldUserViewModel()

    var body: some View {
        VStack {
            Text("Name: \(viewModel.name)") // Observes 'name'
                .font(.title)
            Text("Age: \(viewModel.age)")   // Observes 'age'
            
            Button("Change Name") {
                viewModel.updateName("Jane Doe")
            }
        }
    }
}

// Migrated to @Observable
@Observable
class NewUserViewModel {
    var name: String = "John Doe"
    var age: Int = 30
    var email: String? = nil
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    func updateName(_ newName: String) {
        name = newName
    }
}

struct NewUserView: View {
    // Use @State for ownership, or pass in via initializer
    @State var viewModel = NewUserViewModel(name: "John Doe", age: 30)

    var body: some View {
        VStack {
            Text("Name: \(viewModel.name)") // Observes 'name'
                .font(.title)
            Text("Age: \(viewModel.age)")   // Observes 'age'
            
            Button("Change Name") {
                // Only 'name' view will re-render, not 'age'
                viewModel.updateName("Jane Doe")
            }
        }
        .navigationTitle("User Profile")
    }
}

Understanding @State with @Observable

One of the most significant changes when moving to @Observable is how you manage the ownership of your observable objects. With ObservableObject, @StateObject was the primary solution for creating and owning instances of your observable classes within a view hierarchy.

With @Observable, you no longer use @StateObject, @ObservedObject, or @EnvironmentObject. Instead, you use the fundamental @State property wrapper to manage the lifecycle of your @Observable objects when they are owned by a view. This feels more aligned with @State's original purpose of managing view-specific state.

When you declare an @Observable object using @State, SwiftUI ensures that the object is created once when the view is initialized and persists across view updates, similar to how @StateObject functioned. If you want to pass an @Observable object down to a child view without owning it, you simply declare it as a regular property. SwiftUI automatically observes its changes if it's accessed within the child's body.

For passing shared Observable objects down the environment, you use the new environment(_:_:) modifier and the @Environment property wrapper, but this time, @Environment takes the type of the observable property as its generic parameter, making it more type-safe and performant.

iOS/macOS Compatibility: @Observable requires iOS 17.0+, macOS 14.0+, tvOS 17.0+, watchOS 10.0+.

swift
import SwiftUI

@Observable
class AppSettings {
    var theme: String = "Light"
    var notificationsEnabled: Bool = true
}

struct ParentView: View {
    // Owning an @Observable object with @State
    @State private var settings = AppSettings()

    var body: some View {
        VStack {
            Text("Theme: \(settings.theme)")
            Toggle("Notifications", isOn: $settings.notificationsEnabled)
            
            ChildView(settings: settings) // Passing as a regular property
        }
        .padding()
        .environment(settings) // Making it available to environment
    }
}

struct ChildView: View {
    // Receiving an @Observable object as a regular property
    var settings: AppSettings

    var body: some View {
        VStack {
            Text("Child Theme: \(settings.theme)")
            Button("Toggle Theme") {
                settings.theme = settings.theme == "Light" ? "Dark" : "Light"
            }
        }
        .padding()
        .border(.blue)
    }
}

struct GrandchildView: View {
    // Receiving via environment
    @Environment(AppSettings.self) var settings

    var body: some View {
        VStack {
            Text("Grandchild Notifs: \(settings.notificationsEnabled ? "On" : "Off")")
        }
        .padding()
        .border(.purple)
    }
}

// To see GrandchildView in action, embed it:
struct RootView: View {
    var body: some View {
        ParentView()
    }
}

Performance Benefits and Granular Updates

The primary performance advantage of @Observable stems from its granular observation capabilities. With ObservableObject and @Published, any change to any @Published property within a model would typically cause all views observing that model to re-render. This often led to unnecessary view updates, especially in complex UIs.

@Observable, however, tracks property access at a much finer grain. When a view accesses a property of an @Observable instance, SwiftUI records that specific property as a dependency. If only that property changes, only the views (or parts of views) that actually utilize that property's value will update. Other views observing the same @Observable object but not the changed property remain untouched. This significantly reduces computation and rendering cycles, leading to smoother animations and a more responsive user interface.

For example, if you have a User model with name and email properties, and a view displays only the user's name, changing the email property will not cause that name-displaying view to re-render. This was not reliably true with ObservableObject.

This granular tracking is transparent to the developer – you simply declare your model with @Observable, and SwiftUI handles the optimizations automatically. It's a powerful abstraction that improves performance without adding complexity to your code.

Advanced Usage and Best Practices

While @Observable simplifies many aspects of data observation, understanding some advanced patterns and best practices will help you get the most out of it.

  1. Computed Properties: Computed properties within an @Observable class will automatically trigger updates if any of the underlying stored properties they depend on change. This works seamlessly.
  2. Referencing other @Observable objects: If an @Observable object contains another @Observable object, SwiftUI will follow the dependency chain. Changes in the nested observable will propagate up.
  3. Combine Integration: For existing Combine publishers, you can still bridge them into your @Observable models by updating a stored property when the publisher emits. SwiftUI will then observe the stored property.
  4. @ObservationIgnored: If you have properties within an @Observable class that should not trigger view updates (e.g., internal caches, delegates), mark them with @ObservationIgnored. This prevents SwiftUI from tracking changes to these properties and optimizes performance further.
  5. withObservationTracking: For scenarios where you need to manually observe changes outside of a View's body (e.g., in a UIViewControllerRepresentable or a custom rendering engine), SwiftUI provides withObservationTracking. This function allows you to explicitly define a block of code and a change handler that will be called if any observed properties within the block change.
  6. @Entry Point for Observation: Remember that an @Observable object must be observed from an @ViewBuilder context for its changes to trigger view updates. Simply changing a property won't do anything unless a view is actively watching it. Typically, this means referencing it within a View's body or init (for a View's init that's passed an @Observable type).
swift
import SwiftUI
import Combine

@Observable
class NetworkMonitor {
    enum ConnectionStatus {
        case connected, disconnected, unknown
    }
    
    var status: ConnectionStatus = .unknown
    @ObservationIgnored var reachabilityPublisher: AnyCancellable?
    
    init() {
        // Simulate a network connection publisher
        reachabilityPublisher = Timer.publish(every: 5, on: .main, in: .common)
            .autoconnect()
            .scan(false) { (current, _) in !current }
            .map { isConnected in
                isConnected ? ConnectionStatus.connected : ConnectionStatus.disconnected
            }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] newStatus in
                self?.status = newStatus // Update @Observable property
            }
    }
}

@Observable
class DetailedUser {
    var firstName: String
    var lastName: String
    var age: Int
    
    // Computed property that depends on stored properties
    var fullName: String {
        "\(firstName) \(lastName)"
    }
    
    // @ObservationIgnored property won't trigger view updates
    @ObservationIgnored var lastSaveTimestamp: Date = Date()
    
    init(firstName: String, lastName: String, age: Int) {
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
    }
    
    func save() {
        lastSaveTimestamp = Date()
        print("User saved at: \(lastSaveTimestamp)") // This won't cause UI update if only `lastSaveTimestamp` is observed
    }
}

struct DetailedUserView: View {
    @State var user = DetailedUser(firstName: "Alice", lastName: "Smith", age: 28)
    @State var monitor = NetworkMonitor()

    var body: some View {
        VStack(spacing: 20) {
            Text("Full name: \(user.fullName)") // Observes only 'firstName' and 'lastName'
                .font(.title)
            Text("Name: \(user.firstName)")
            Text("Age: \(user.age)") // Observes 'age'
            
            Button("Change Age") {
                user.age += 1
            }
            
            Button("Change Name") {
                user.firstName = "Alicia"
                user.lastName = "Johnson"
            }
            
            Divider()
            Text("Network Status: \(String(describing: monitor.status))")
            
            if monitor.status == .disconnected {
                Text("Please check your connection!")
                    .foregroundColor(.red)
            }
        }
        .padding()
    }
}

`ObservableObject` Performance

Becoming a stronger iOS Engineer

THE MYTH or PROBLEM: `ObservableObject` Performance

`ObservableObject` with `@Published` often caused entire views observing the object to re-render, even if only an unused property changed. This led to inefficient UI updates and potential performance bottlenecks in complex SwiftUI apps, requiring manual optimization (e.g., `Equatable` conformance or `_` accessors).

WHAT HAPPENS INTERNALLY? `@Observable` Mechanism

`@Observable` leverages Swift macros to synthesize observation code at compile time. When a view accesses a property of an `@Observable` instance, a hidden mechanism records this access. When that *specific* property is later set to a new value, the observation system is notified, and only views that accessed that specific property are marked for re-evaluation.

View Hierarchy (Observation Scope)
@Observable Model Instance
Property Access A (e.g. `user.name`)
Property Access B (e.g. `user.email`)
1

1. Macro Expansion

`@Observable` macro adds observation conformance and backing storage.

2

2. View Access

SwiftUI's runtime tracks property reads within an observation scope (e.g., `View.body`).

3

3. Dependency Registration

Specific property (e.g., `user.name`) is added to the view's dependency list.

4

4. Property Mutation

When `user.name` changes, the observation system is notified.

5

5. Targeted Re-evaluation

Only views dependent on `user.name` are re-evaluated for updates.

Visualized execution hierarchy.

Powerful Guarantees

Granular View Updates

Only parts of the UI truly dependent on a changed property will re-render, leading to significant performance over `ObservableObject`.

Reduced Boilerplate

No more `ObservableObject` protocol or `@Published` wrappers needed. Macros handle the conformity automatically.

`@State` Simplification

Unifies state management; `@State` now manages persistence of `Observable` objects, removing the need for `@StateObject`.

REAL PRODUCTION EXAMPLE: Chat Message List

Imagine a `ChatRoomViewModel` with `var messages: [ChatMessage]` and `var draftMessage: String`. With `ObservableObject`, if a new message arrives (updates `messages`), the UI elements displaying the `draftMessage` might unnecessarily re-render. Similarly, typing in `draftMessage` might re-render the entire message list.

Impact / Results
Smoother typing experience (draftMessage updates don't impact message list)
Efficient message appending (new messages don't re-render unrelated UI)
Lower CPU/GPU usage
THE FIX or SOLUTION: `@Observable` for Granularity
swift
import SwiftUI

@Observable
class ChatRoomViewModel {
    var messages: [String] = []
    var draftMessage: String = ""

    func sendMessage() {
        guard !draftMessage.isEmpty else { return }
        messages.append(draftMessage)
        draftMessage = "" // Only 'draftMessage' UI updates
    }
    
    func receiveMessage(_ message: String) {
        messages.append(message) // Only 'messages' UI updates
    }
}

struct ChatView: View {
    @State var viewModel = ChatRoomViewModel()

    var body: some View {
        VStack {
            ScrollView {
                ForEach(viewModel.messages, id: \.self) { message in
                    Text(message) // Only observes 'viewModel.messages'
                }
            }
            .frame(maxHeight: .infinity)

            HStack {
                TextField("New message", text: $viewModel.draftMessage) // Only observes 'viewModel.draftMessage'
                    .textFieldStyle(.roundedBorder)
                Button("Send") {
                    viewModel.sendMessage()
                }
            }
            .padding()
        }
        .onAppear {
            viewModel.receiveMessage("Welcome!")
        }
    }
}

INTERVIEW PERSPECTIVE

Common Question

“Explain the advantages of `@Observable` over `ObservableObject`. When would you choose one over the other?”

Strong Answer

A strong answer should highlight `@Observable`'s *granular observation*, *reduced boilerplate* via macros, *performance benefits* (only rendering what's needed), and simpler *state management* paradigms (using `@State`). While `@Observable` is the modern choice for new code on iOS 17+, you might stick with `ObservableObject` for backward compatibility or if integrating with existing Combine-heavy architectures where direct `@Published` publishers are beneficial, although `@Observable` can also integrate with Combine.

Interviewers Expect you to understand:
  • Granular observation
  • Automatic dependency tracking
  • Compiler-provided implementation (macros)
  • Backward compatibility (pre-iOS 17)
KEY TAKEAWAY

Embrace `@Observable` for modern SwiftUI data flow. It's more performant, less verbose, and simplifies state management compared to its `ObservableObject` predecessor, making your apps more efficient and delightful to build.

Common Interview Questions

What is the primary difference between `ObservableObject` and `@Observable`?

`@Observable` offers more granular observation, meaning views only re-render when the *specific properties* they depend on change. `ObservableObject` often caused views to re-render when *any* `@Published` property changed, even if the view didn't use it, leading to less efficient updates. `@Observable` also generates conformances automatically via macros, reducing boilerplate.

Do I still use `@StateObject` with `@Observable` classes?

No. With `@Observable` classes, you should use the `@State` property wrapper for owning and managing the lifecycle of your observable objects within a view. `@StateObject`, `@ObservedObject`, and `@EnvironmentObject` are for `ObservableObject` and are not used with `@Observable`.

Can I use `@Observable` with structs?

While technically you can apply `@Observable` to a `struct`, for it to behave reactively like a class concerning view updates, it must be a *reference type* within SwiftUI's observation system. This typically means wrapping it in a `State` or passing it by reference implicitly. However, the common and recommended pattern for `@Observable` is to use it with `class` types, as their reference semantics align naturally with shared state and observation. Using it on a `struct` directly as view state would simply make the `struct` itself observable, not its properties changes when copied.

How do I make a property within an `@Observable` class *not* trigger view updates?

You can mark that specific property with the `@ObservationIgnored` macro. This tells SwiftUI's observation system to disregard changes to that property, preventing it from causing view re-renders.

What is `withObservationTracking` used for?

`withObservationTracking` is a new global function that allows you to manually establish an observation scope. You provide a block of code (the `keyPaths` property accesses within which will be observed) and a change handler. The handler will be called if any of the observed properties change, enabling reactive behavior outside of SwiftUI `View` bodies, like in `UIViewControllerRepresentable` or custom event handlers.

#SwiftUI#@Observable#Observation#Data Flow#Property Wrappers#Swift 5.9