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 ObservableObject in SwiftUI for Dynamic UI Updates

ObservableObject is a cornerstone of state management in SwiftUI, enabling your views to react dynamically to changes in your data models. By understanding its mechanics, you can build more robust, performant, and maintainable SwiftUI applications. This guide will walk you through everything you need to know.

Understanding SwiftUI State Management

Before diving into ObservableObject, it's crucial to grasp SwiftUI's overall approach to state management. SwiftUI is a declarative framework, meaning you describe what your UI should look like for a given state, and the framework takes care of updating it when the state changes. This reactive paradigm relies heavily on a system of property wrappers and protocols designed to keep your UI in sync with your underlying data.

@State, @Binding, @EnvironmentObject, and @ObservedObject are primary tools for managing various forms of state. While @State is excellent for simple, view-local value types, ObservableObject steps in when you need to manage more complex, shared reference types that can influence multiple parts of your application.

ObservableObject works in tandem with the Combine framework. When a property marked with @Published within an ObservableObject changes, it automatically publishes an event. SwiftUI, in turn, subscribes to these events and re-renders the affected views, ensuring your UI always reflects the latest data. This automatic synchronization greatly simplifies your view logic and reduces the chance of manual update errors.

What is ObservableObject?

ObservableObject is a protocol that a class can conform to, enabling SwiftUI to observe changes to its properties. When a class conforms to ObservableObject, it gains the ability to emit notifications whenever certain properties within it change. SwiftUI views that depend on an instance of this class can then automatically re-render when those notifications are received.

To make a property within an ObservableObject class trigger these updates, you must mark it with the @Published property wrapper. This wrapper, part of the Combine framework, automatically synthesizes a Publisher for the property, allowing SwiftUI to subscribe to its changes.

Let's look at a basic example of an ObservableObject and how it's used:

swift
import SwiftUI
import Combine

// 1. Define an ObservableObject class
class UserSettings: ObservableObject {
    // 2. Mark properties with @Published to trigger UI updates
    @Published var username: String = "Guest"
    @Published var isPremium: Bool = false
    
    func togglePremium() {
        isPremium.toggle()
        print("Premium status changed to: \(isPremium)")
    }
}

// 3. Use @ObservedObject or @StateObject in a SwiftUI View
struct UserProfileView: View {
    // Use @StateObject for single source of truth in the view's lifecycle
    @StateObject var settings = UserSettings()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Welcome, \(settings.username)!")
                .font(.largeTitle)
            
            Toggle(isOn: $settings.isPremium) {
                Text("Premium Member")
            }
            .padding()
            .onChange(of: settings.isPremium) { oldValue, newValue in
                // You can react to changes here as well, though @Published handles UI updates.
                print("Premium status changed from \(oldValue) to \(newValue)")
            }
            
            Button("Change Username") {
                settings.username = "SwiftUI Master"
            }
            
            Button("Toggle Premium (via function)") {
                settings.togglePremium()
            }
        }
        .padding()
        .navigationTitle("Settings")
    }
}

struct UserProfileView_Previews: PreviewProvider {
    static var previews: some View {
        UserProfileView()
    }
}

When to Use @ObservedObject vs. @StateObject

Choosing between @ObservedObject and @StateObject is a critical decision that impacts the lifecycle and behavior of your ObservableObject instances.

  • @StateObject (Introduced in iOS 14.0, macOS 11.0): This property wrapper is designed for ownership. When you create an ObservableObject instance using @StateObject within a view, SwiftUI takes ownership of that object. This means the object will be created only once for the lifetime of the view, even if the view itself is re-rendered or invalidated. Use @StateObject when a view 'owns' the source of truth for a particular ObservableObject instance.

    Use Case: Creating a new ViewModel instance directly within a root view or a view that needs to manage a complex local state that persists across view updates.

  • @ObservedObject: This property wrapper is for instances of ObservableObject that are passed in from an external source (e.g., from a parent view, or via dependency injection). A view using @ObservedObject does not own the object; it merely observes it. If the view is re-rendered and the parent view provides a new instance of the ObservableObject, the @ObservedObject will update to point to the new instance, potentially losing prior state.

    Use Case: When a parent view creates or owns an ObservableObject and passes it down to a child view. The child view then observes changes to that pre-existing object.

Key Difference: @StateObject ensures the object persists as long as the view struct instance exists. @ObservedObject does not; it expects the object to be managed externally.

swift
import SwiftUI

// Shared ObservableObject for demonstration
class DataStore: ObservableObject {
    @Published var counter: Int = 0
    
    init() {
        print("DataStore initialized")
    }
    
    deinit {
        print("DataStore deinitialized")
    }
    
    func increment() {
        counter += 1
    }
}

// Parent view owning the DataStore via @StateObject
struct ParentView: View {
    @StateObject var data = DataStore()
    @State private var showChild: Bool = false
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Parent Counter: \(data.counter)")
            Button("Increment Parent") {
                data.increment()
            }
            
            Button("Toggle Child View") {
                showChild.toggle()
            }
            
            if showChild {
                // Child_ObservedObject receives the SAME instance.
                // It will NOT create a new DataStore if ParentView re-renders.
                Child_ObservedObject(dataStore: data)
            }
        }
        .padding()
        .navigationTitle("Parent View")
    }
}

// Child view observing the DataStore passed in
struct Child_ObservedObject: View {
    @ObservedObject var dataStore: DataStore // Receives existing object
    
    var body: some View {
        VStack {
            Text("Child Counter: \(dataStore.counter)")
            Button("Increment Child") {
                dataStore.increment()
            }
        }
        .padding()
        .background(Color.blue.opacity(0.2))
        .cornerRadius(8)
        .id(UUID()) // Force recreation on identity change (for testing purposes only)
    }
}

// A view that incorrectly uses @StateObject outside its ownership context
struct Child_IncorrectStateObject: View {
    // THIS IS GENERALLY INCORRECT when 'dataStore' is passed in.
    // It will create a NEW DataStore if the view is recreated, ignoring the passed-in one.
    @StateObject var dataStore: DataStore
    
    var body: some View {
        VStack {
            Text("Incorrect Child Counter: \(dataStore.counter)")
            Button("Increment Incorrect Child") {
                dataStore.increment()
            }
        }
        .padding()
        .background(Color.red.opacity(0.2))
        .cornerRadius(8)
    }
}

struct StateObjectVsObservedObject_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            ParentView()
        }
    }
}

Using ObservableObject with @EnvironmentObject

@EnvironmentObject (introduced in iOS 13.0, macOS 10.15) provides an even more convenient way to share ObservableObject instances across an entire view hierarchy without manually passing them down through initializer parameters. It leverages SwiftUI's environment, allowing any descendant view to access a provided object.

To use @EnvironmentObject:

  1. Conform to ObservableObject: Your data model class must conform to ObservableObject and use @Published for its properties.
  2. Inject into Environment: A parent view (or your App struct) uses the .environmentObject() modifier to place an instance of your ObservableObject into the environment.
  3. Retrieve from Environment: Any descendant view can then use @EnvironmentObject to declare a dependency on that specific type of ObservableObject.

This pattern is ideal for global application-wide state, like a user session, theme settings, or a data store that many views need to access.

swift
import SwiftUI

class AppSettings: ObservableObject {
    @Published var themeColor: Color = .blue
    @Published var currentUser: String = ""
    
    init() {
        // Simulate loading user settings
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.currentUser = "Alice Doe"
            self.themeColor = .purple
        }
    }
}

struct ContentView: View {
    // Inject AppSettings into the environment at the App's root or a high-level view.
    // The @StateObject here ensures AppSettings persists for the life of ContentView.
    @StateObject private var appSettings = AppSettings()
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Text("Root View")
                    .font(.largeTitle)
                
                MainDashboardView()
            }
            .navigationTitle("Home")
            .background(appSettings.themeColor.opacity(0.1).ignoresSafeArea())
        }
        .environmentObject(appSettings) // Inject the object here
        // The .environmentObject() modifier is available from iOS 13.0+
    }
}

struct MainDashboardView: View {
    var body: some View {
        VStack {
            Text("Dashboard Content")
            UserInfoView()
            ThemeSettingsView()
        }
    }
}

struct UserInfoView: View {
    // Access the environment object directly
    @EnvironmentObject var settings: AppSettings
    
    var body: some View {
        Text("Logged in as: \(settings.currentUser)")
            .font(.title2)
            .onChange(of: settings.currentUser) { oldValue, newValue in
                print("User changed from \(oldValue) to \(newValue)")
            }
    }
}

struct ThemeSettingsView: View {
    @EnvironmentObject var settings: AppSettings
    
    var body: some View {
        VStack {
            Text("Current Theme Color: \(settings.themeColor.description)")
            Button("Change Theme to Green") {
                settings.themeColor = .green
            }
            Button("Change Theme to Orange") {
                settings.themeColor = .orange
            }
        }
        .padding()
        .background(settings.themeColor.opacity(0.3))
        .cornerRadius(10)
    }
}

struct EnvironmentObjectExample_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Manual Publishing with objectWillChange

While @Published is convenient for most properties, there are scenarios where you might need more fine-grained control over when an ObservableObject announces changes. For instance:

  • When a computed property changes but doesn't have an underlying @Published stored property.
  • When changes occur within a complex nested data structure that isn't itself an ObservableObject.
  • When you want to batch multiple changes before announcing them.

In such cases, ObservableObject provides a publisher called objectWillChange. You can manually send a value through this publisher to signal to SwiftUI that changes are about to occur, prompting a view update.

The objectWillChange publisher is of type ObservableObjectPublisher and is automatically synthesized by the ObservableObject protocol. You can access it directly to call its send() method.

swift
import SwiftUI
import Combine

class DataProcessor: ObservableObject {
    // A private property that isn't @Published
    private var _processingStepsCompleted: Int = 0 {
        willSet { objectWillChange.send() } // Manually notify observers BEFORE the change
        didSet { print("Processing steps updated to: \(_processingStepsCompleted)") }
    }
    
    // A computed property that depends on _processingStepsCompleted
    var progressMessage: String {
        return "Completed \(_processingStepsCompleted) out of 10 steps."
    }
    
    // An array of complex items, changes to which might not automatically publish.
    // If 'Item' were a struct, changing an element wouldn't trigger update via `objectWillChange` without specific handling.
    // If 'Item' were a reference type and itself ObservableObject, then it would propagate.
    @Published var items: [String] = []
    
    private var timerSubscription: AnyCancellable? = nil
    
    init() {
        // Simulate an asynchronous process
        timerSubscription = Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                guard let self = self else { return }
                if self._processingStepsCompleted < 10 {
                    self._processingStepsCompleted += 1
                    // No need to send() for @Published properties, but _processingStepsCompleted does it manually.
                    if self._processingStepsCompleted == 3 {
                        self.items.append("Data point A added")
                    }
                    if self._processingStepsCompleted == 7 {
                        self.items.append("Data point B added")
                    }
                } else {
                    self.timerSubscription?.cancel()
                }
            }
    }
    
    deinit {
        timerSubscription?.cancel()
    }
}

struct ManualPublishingView: View {
    @StateObject var processor = DataProcessor()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Processing Status")
                .font(.title)
            
            Text(processor.progressMessage)
                .font(.headline)
            
            List(processor.items, id: \.self) {
                Text($0)
            }
            .frame(height: 150)
            .border(Color.gray)
            
            if processor.progressMessage.contains("10 out of 10") {
                Text("Process Complete!")
                    .font(.title2)
                    .foregroundColor(.green)
            }
        }
        .padding()
        .navigationTitle("Manual Publish")
    }
}

struct ManualPublishingView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            ManualPublishingView()
        }
    }
}

Best Practices for ObservableObject

To build robust and maintainable SwiftUI applications using ObservableObject, consider these best practices:

  • Keep Model Logic Separate: Your ObservableObject classes should primarily contain business logic, data fetching, and state management. Keep view-specific logic within your SwiftUI views.
  • Use @StateObject for Ownership: Always use @StateObject when a view is responsible for creating and owning an ObservableObject. This ensures its lifecycle is tied to the view's.
  • Use @ObservedObject for Observation: Use @ObservedObject when an ObservableObject is passed into a view, indicating that the view observes an object owned elsewhere.
  • Use @EnvironmentObject for Global State: For application-wide data or services, EnvironmentObject offers a clean way to inject dependencies deeply into your view hierarchy.
  • Minimize @Published Properties: Only mark properties with @Published if changes to them should directly trigger UI updates. Avoid marking internal helper properties that don't affect the UI.
  • Thread Safety: Remember that ObservableObject instances are typically accessed and modified on the main thread when driving UI updates. If you perform background work (e.g., network calls, heavy computations) that updates @Published properties, ensure these updates are dispatched back to the main queue (e.g., DispatchQueue.main.async { self.someProperty = newValue }) to prevent UI inconsistencies or crashes. Combine's receive(on:) operator is excellent for this.
  • View Model Pattern: ObservableObject is the fundamental building block for the 'View Model' in the MVVM-C (Model-View-ViewModel-Coordinator) architectural pattern, a popular choice for SwiftUI applications. Your ObservableObject acts as the bridge between your Model (raw data) and your View.
  • Testing: Design your ObservableObject classes to be easily testable. This often means making dependencies injectable so you can mock them during unit tests.

Stale UI & Complex Data Models

Mastering SwiftUI State with ObservableObject

THE PROBLEM: Stale UI & Complex Data Models

Without a proper mechanism, SwiftUI views don't automatically update when data in a complex, shared class changes. Manually propagating changes is error-prone and leads to boilerplate code and inconsistent UI states.

swift
class MyData {
    var value: Int = 0
}

struct MyView: View {
    @State var data = MyData() // This won't react to 'value' changes in MyData
    var body: some View {
        Text("Value: \(data.value)") // Stale if data.value changes internally
    }
}

WHAT HAPPENS INTERNALLY?

`ObservableObject` utilizes the Combine framework to provide a robust observation mechanism. When an `ObservableObject` property marked `@Published` changes, it sends a signal through its `objectWillChange` publisher. SwiftUI views listening with `@ObservedObject`, `@StateObject`, or `@EnvironmentObject` automatically re-render based on this signal.

ObservableObject (Your Class)
@Published Property 1
@Published Property 2
objectWillChange Publisher
1

1. Data Model Conforms

Your class conforms to `ObservableObject` protocol.

2

2. Properties @Published

Specific properties are marked with `@Published`, creating Combine publishers.

3

3. Value Changes

A `@Published` property's value is modified.

4

4. Publisher Emits

The associated Combine publisher emits a new value via `objectWillChange`.

5

5. SwiftUI Reacts

SwiftUI views invalidates their bodies and re-render to reflect the new state.

Visualized execution hierarchy.

Powerful Guarantees

Automatic UI Sync

Changes to `@Published` properties trigger view re-renders automatically.

Reference Semantics

Allows sharing a single instance of data across multiple views.

Lifecycle Management

`@StateObject` ensures model persistence for view lifespan.

Type Safety

Combines with Swift's type system for robust data models.

REAL PRODUCTION EXAMPLE: User Profile Data

In an app with a user profile, multiple screens (e.g., Profile View, Settings View, Dashboard Widget) need to display and modify the same user information. Without `ObservableObject`, propagating updates would be a nightmare. With it, all dependent views automatically refresh.

Impact / Results
Consistent user data across all app screens
Reduced boilerplate for data propagation
Easier to manage complex user states (e.g., subscription status, notifications)
THE FIX: Using ObservableObject
swift
class UserProfileViewModel: ObservableObject {
    @Published var name: String = "" {
        didSet { saveUserData() }
    }
    @Published var email: String = ""
    @Published var isSubscriber: Bool = false

    init() { loadUserData() }

    private func loadUserData() { /* ... */ }
    private func saveUserData() { /* ... */ }
    
    func updateSubscription(status: Bool) {
        self.isSubscriber = status
    }
}

struct ProfileView: View {
    @StateObject var viewModel = UserProfileViewModel() // Owned by ProfileView

    var body: some View {
        VStack {
            TextField("Name", text: $viewModel.name)
            Text("Subscription: \(viewModel.isSubscriber ? "Active" : "Inactive")")
            // ... other UI elements bound to viewModel
        }
    }
}

// Child view, observing but not owning
struct SubscriptionStatusView: View {
    @ObservedObject var viewModel: UserProfileViewModel

    var body: some View {
        Toggle(isOn: $viewModel.isSubscriber) {
            Text("Subscribe to Premium")
        }
    }
}

INTERVIEW PERSPECTIVE

Common Question

“Explain the role of `ObservableObject` in SwiftUI's state management and differentiate between `@StateObject` and `@ObservedObject`.”

Strong Answer

`ObservableObject` is a protocol for reference types (`class`) that allows SwiftUI views to react to changes in their properties. It's the foundation for managing shared, complex state. `@StateObject` is for 'owning' an `ObservableObject` instance, ensuring it lives and persists with the view's lifecycle. `@ObservedObject` is for 'observing' an `ObservableObject` that's owned and managed externally, useful when an instance is passed down from a parent view. Both leverage Combine's `@Published` for automatic UI updates.

Interviewers Expect you to understand:
  • Protocol for classes
  • Leverages `@Published` and Combine
  • Enables reactive UI updates
  • Differentiates `@StateObject` (ownership/creation) and `@ObservedObject` (observation)
KEY TAKEAWAY

Use `ObservableObject` for complex, shared, reference-type data models. Create and own instances with `@StateObject`, observe shared instances with `@ObservedObject`, and pass global instances via `@EnvironmentObject` to build dynamic, responsive SwiftUI applications.

Common Interview Questions

What's the main difference between `@State` and `ObservableObject`?

`@State` is used for managing simple, value-type data *owned by a single view*, suitable for things like toggles or text field values that don't need to be shared. `ObservableObject` (used with `@StateObject`, `@ObservedObject`, or `@EnvironmentObject`) is for managing complex, reference-type data models *shared across multiple views* or containing business logic, leveraging Combine for reactive updates.

When should I use `@StateObject` versus `@ObservedObject`?

Use `@StateObject` when the current view *owns* and creates the `ObservableObject` instance, guaranteeing its persistence through view updates. Use `@ObservedObject` when the `ObservableObject` instance is *passed into* the view from a parent or external source, and the view simply observes it without taking ownership.

Can I use `struct` with `ObservableObject`?

No. `ObservableObject` is a protocol that can only be adopted by `class` types. This is because `ObservableObject` relies on reference semantics (i.e., multiple views observing the *same instance* of data) and the ability to publish changes from within that single instance.

How does `ObservableObject` work with Combine?

The `@Published` property wrapper, when used within an `ObservableObject` class, automatically synthesizes a Combine `Publisher` for that property. When the property's value changes, this publisher emits a new value. SwiftUI views then implicitly subscribe to these publishers and re-render themselves when a new value is received, ensuring a reactive UI.

What if my `ObservableObject` performs background tasks?

If your `ObservableObject` updates its `@Published` properties from a background thread (e.g., after a network request), you must ensure those updates are dispatched back to the main thread. You can do this using `DispatchQueue.main.async { ... }` or Combine's `receive(on: DispatchQueue.main)` operator to prevent UI glitches or crashes.

#SwiftUI#ObservableObject#State Management#Combine#Swift