Swift and SwiftUI tutorials for Swift Developers

@Observable vs ObservableObject in SwiftUI

For any iOS Developer, state management is, without a doubt, the heart and brain of any application. Since the release of SwiftUI, Apple introduced us to a reactive paradigm where the user interface is a direct function of its state. For years, the gold standard in Swift programming for handling complex states was the ObservableObject protocol.

However, with the arrival of Swift 5.9, Xcode 15, and the Observation framework, Apple introduced the @Observable macro, changing the rules of the game forever.

In this comprehensive tutorial, we will delve into what they are, how they work, and what the differences and similarities are between @Observable and ObservableObject in SwiftUI for developing cross-platform applications across iOS, macOS, and watchOS.


1. The State Paradigm in Swift Programming

Before diving into the code, it is vital to understand the problem both tools are trying to solve. In SwiftUI, views are structs, meaning they are immutable value types. When you need your interface to react to changes in complex data that outlives the lifecycle of a single view—such as a user profile, a shopping cart, or business logic (ViewModels)—you need classes (reference types).

Both ObservableObject and @Observable act as bridges that allow SwiftUI views to “observe” these classes and automatically redraw (render) themselves when the data changes.


2. What is ObservableObject? (The Classic Approach)

Introduced in the very first version of SwiftUI (iOS 13, macOS 10.15, watchOS 6), ObservableObject is a protocol that belongs to the Combine framework.

How does it work?

When a class conforms to the ObservableObject protocol, Swift automatically provides it with a hidden publisher called objectWillChange. For SwiftUI to know which specific properties should trigger a UI update, the iOS Developer must explicitly mark them with the @Published Property Wrapper.

Swift Example with ObservableObject:

import SwiftUI
import Combine

// 1. Conform to the ObservableObject protocol
class UserViewModel: ObservableObject {
    // 2. Use @Published to notify changes
    @Published var username: String = "iOS_Dev_123"
    @Published var followerCount: Int = 0
    
    // Normal property: changes here will NOT update the UI
    var sessionID: String = UUID().uuidString 
    
    func addFollower() {
        followerCount += 1
    }
}

struct UserProfileView: View {
    // 3. Instantiate using @StateObject or @ObservedObject
    @StateObject private var viewModel = UserViewModel()
    
    var body: some View {
        VStack {
            Text("User: \(viewModel.username)")
            Text("Followers: \(viewModel.followerCount)")
            Button("New Follower") {
                viewModel.addFollower()
            }
        }
    }
}

Historical Advantages:

  • Allowed for a clean implementation of the MVVM (Model-View-ViewModel) pattern in SwiftUI.
  • Compatible with all versions of SwiftUI since its inception.
  • Deep, native integration with Combine’s reactive programming.

3. What is @Observable? (The Present and Future)

Introduced at WWDC23 (for iOS 17, macOS 14, and watchOS 10), @Observable is not a protocol, but a Swift Macro. It utilizes the new Observation framework, which is written entirely in pure Swift and does not rely on Combine.

How does it work?

When you add @Observable before your class declaration, Xcode and the Swift compiler expand that macro at compile time to inject a highly efficient tracking system. You no longer need to declare which properties will update the UI; by default, all stored properties are observable.

SwiftUI automatically registers which properties are “read” inside a view’s body and will only redraw that view if those specific properties change.

Swift Example with @Observable:

import SwiftUI
import Observation // Imported automatically with SwiftUI in Xcode 15+

// 1. Add the @Observable macro
@Observable 
class UserViewModel {
    // 2. Properties are observable by default. Goodbye @Published!
    var username: String = "iOS_Dev_123"
    var followerCount: Int = 0
    
    // If you DO NOT want a property to be observable, use @ObservationIgnored
    @ObservationIgnored var sessionID: String = UUID().uuidString 
    
    func addFollower() {
        followerCount += 1
    }
}

struct UserProfileView: View {
    // 3. Instantiate using simply @State
    @State private var viewModel = UserViewModel()
    
    var body: some View {
        VStack {
            Text("User: \(viewModel.username)")
            Text("Followers: \(viewModel.followerCount)")
            Button("New Follower") {
                viewModel.addFollower()
            }
        }
    }
}

4. Similarities between @Observable and ObservableObject in SwiftUI

Despite their architectural differences, for an iOS Developer migrating from one to the other, the foundational concepts remain intact:

  1. Reference Type Management: Both are used exclusively on classes (class). You cannot use them on structs or enums.
  2. Main Purpose: Both notify SwiftUI when it needs to invalidate and recalculate a view’s body due to a data change.
  3. Cross-Platform Support: Both work seamlessly across the entire Apple ecosystem, allowing you to share the same business logic (ViewModel) across your Xcode targets for iOS, macOS, tvOS, visionOS, and watchOS.
  4. Architectural Patterns: Both facilitate architectures like MVVM, allowing you to keep business logic and network calls separated from the visual SwiftUI code.

5. Key Differences: The Ultimate Battle

This is where the true value of this tutorial lies. Understanding these differences is what separates a junior developer from a Senior iOS Developer in modern Swift programming.

5.1. Performance and Granularity (The Big Upgrade)

The most critical difference between @Observable and ObservableObject in SwiftUI is how and when they decide to update the views.

  • The ObservableObject problem: When any @Published property changes, objectWillChange.send() is emitted. SwiftUI invalidates any view observing that object, regardless of whether the view actually uses that specific property. This often causes unnecessary re-renders and frame rate (FPS) drops in complex views.
  • The magic of @Observable: It uses Access Tracking. If your class has 20 properties, but the view only reads username, the view will only be redrawn when username changes. If followerCount changes, the view will ignore the event because it knows it doesn’t rely on that data. This offers optimal granular performance with no extra work required from the developer.

5.2. Framework Dependency

  • ObservableObject requires importing Combine. It is a bridge between the world of reactive programming (Publishers/Subscribers) and SwiftUI.
  • @Observable belongs to the Observation framework, which is part of the standard Swift library. This makes the code purer, more portable, and faster to compile in Xcode.

5.3. Syntax and Boilerplate

Swift programming is constantly striving to be cleaner and more readable:

  • ObservableObject: Requires conforming to the protocol explicitly and adding the @Published tag to every variable you want to observe. If you forget a @Published, you will spend hours debugging why your UI isn’t updating.
  • @Observable: Being a Swift macro, it is placed once above the class. It intelligently assumes you want to observe everything. If you want to exclude something (for performance or logic reasons), you use @ObservationIgnored. The code is significantly cleaner.

5.4. Handling Arrays and Collections

Historically, arrays in ObservableObject were a headache. If you had an @Published var users: [User] (where User is a class), adding a user to the array updated the UI, but changing a property inside an existing user (e.g., users[0].name = "New") did not trigger an update in SwiftUI.
With @Observable, deep property tracking and iteration work much more naturally and predictably.


6. Evolution of Property Wrappers in SwiftUI

By changing the observation engine, the way we inject dependencies and pass data between views in Xcode also changed radically. If you are making the jump to @Observable, you need to learn the new Property Wrapper mapping.

From @StateObject to @State

Previously, to instantiate (create) an ObservableObject and ensure it wasn’t destroyed during view redraws, we used @StateObject.
Now, with the @Observable macro, SwiftUI unifies the concept: you use @State for both simple values (String, Int) and your observable classes.

// BEFORE (ObservableObject)
@StateObject var viewModel = MyViewModel()

// NOW (@Observable)
@State var viewModel = MyViewModel()

From @ObservedObject to Nothing (or @Bindable)

Previously, to pass an existing object from a parent view to a child view, we used @ObservedObject.
Now, you simply pass the property using a normal let or var! Since tracking happens internally, SwiftUI knows if it should update the view.

// BEFORE
struct ChildView: View {
    @ObservedObject var viewModel: MyViewModel
    // ...
}

// NOW (Read-Only Mode)
struct ChildView: View {
    let viewModel: MyViewModel 
    // ...
}

Important Exception: If the child view needs to modify the values directly (for example, passing it to a TextField), you must use @Bindable to create two-way read/write connections (Bindings).

// NOW (Read and Write Mode)
struct ChildView: View {
    @Bindable var viewModel: MyViewModel
    
    var body: some View {
        TextField("Name", text: $viewModel.username)
    }
}

From @EnvironmentObject to @Environment

The global SwiftUI environment is essential for data like user sessions or visual themes in iOS, macOS, and watchOS apps.
Previously, you injected with .environmentObject() and read with @EnvironmentObject.
Now, you inject with .environment() and read more safely with @Environment.

// BEFORE
// Injection: ContentView().environmentObject(themeManager)
@EnvironmentObject var themeManager: ThemeManager

// NOW
// Injection: ContentView().environment(themeManager)
@Environment(ThemeManager.self) private var themeManager

7. When to Use Which in Your Xcode Projects?

As an iOS Developer, making architectural decisions is part of your daily routine. Although @Observable is objectively superior, the choice depends on your project’s context.

Use @Observable (The New Standard) if:

  1. Your project is new and you can afford to set the minimum deployment target to iOS 17, macOS 14, and watchOS 10.
  2. You are experiencing severe performance issues due to massive re-renders caused by ObservableObject in complex views.
  3. You want cleaner code that is less prone to typos and boilerplate errors.

Keep ObservableObject (The Reliable Classic) if:

  1. You have an existing (Legacy) app that must support iOS 16, 15, or earlier versions. @Observable is not backward compatible.
  2. Your business logic relies heavily on complex Combine operators (like debounce, combineLatest, flatMap). Although you can mix Observation and Combine, migrating highly reactive ecosystems requires time and care.
  3. You are developing an open-source library (Package) for Swift programming and want to maximize compatibility with other developers’ projects.

8. Step-by-Step Migration: A Practical Case

If you decide to modernize your app in Xcode, here is a summary of the migration workflow:

  1. Replace the : ObservableObject protocol conformance with the @Observable macro above the class.
  2. Remove import Combine if the class no longer needs it.
  3. Remove all @Published tags from your properties.
  4. Identify the views that instantiate the object and change @StateObject to @State.
  5. In child views that only read data, remove @ObservedObject and leave it as a normal variable (let).
  6. In child views that require Bindings ($), change @ObservedObject to @Bindable.
  7. Update all environment references from @EnvironmentObject to @Environment(Type.self).
  8. Compile and enjoy an immediate boost in your app’s frame rate performance.

Conclusion

The SwiftUI ecosystem is maturing at a breakneck pace. For a modern iOS Developer, mastering Swift programming means understanding not just the syntax, but how the frameworks handle memory and rendering cycles under the hood.

Understanding the coexistence and differences between @Observable and ObservableObject in SwiftUI will allow you to make smarter architectural decisions. While ObservableObject will stick around for a few more years for backward compatibility reasons, the @Observable macro is, undoubtedly, the bright and efficient future of development in the Xcode ecosystem for iOS, macOS, and watchOS.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Create Button with Image in SwiftUI

Next Article

Confirmation Dialog in SwiftUI

Related Posts