@StateObject: What It Is, Difference from @ObservedObject, and Examples

Author: IT Sectr Published: 2026-06-19 Reading time: 7 min

@StateObject is a Property Wrapper in SwiftUI for creating and owning an ObservableObject instance directly in a view. SwiftUI guarantees that the object is initialized once per view’s lifecycle and is not recreated on subsequent re-renders. According to Apple Developer Documentation (2025), @StateObject is recommended for root views that create a data source. @StateObject is the right choice for owning an ObservableObject in the SwiftUI hierarchy.

Key Takeaways

  • @StateObject — Property Wrapper for creating and owning ObservableObject in a view
  • Single instance — the object is created once and not recreated on re-renders
  • Source of truth — @StateObject ensures data stability for the entire hierarchy
  • Difference from @ObservedObject — @ObservedObject does not own the object and can lose it
  • Root views — @StateObject is used in the view that creates the object

What Is @StateObject in SwiftUI?

@StateObject is a Property Wrapper introduced in SwiftUI 2.0 (iOS 14) that combines the capabilities of @ObservedObject and @State. Like @ObservedObject, it subscribes to ObservableObject changes. Like @State, it guarantees that data survives repeated view structure initializations. @StateObject creates the object once when the view first appears and stores it in SwiftUI’s heap.

Before @StateObject, developers used @ObservedObject for all ObservableObjects, including those created in views. This led to frequent data loss when the parent view updated, causing the view structure to be recreated and taking the @ObservedObject instance with it. @StateObject solved this by adding a stability guarantee.

The main rule: @StateObject is used in the view that creates the object in the default initializer (let model = ViewModel()). Child views that receive this object use @ObservedObject. This separation guarantees a single source of truth throughout the hierarchy.

@StateObject Lifecycle

SwiftUI manages the @StateObject lifecycle through a storage manager similar to @State. When the view first appears, SwiftUI allocates memory for the object and stores it in a persistent area. On subsequent re-renders (body calls), the object is not recreated — the existing instance is used. The object lives as long as the view is in the hierarchy.

When the view is removed from the hierarchy, SwiftUI destroys the @StateObject, calling deinit. When the view is added back to the hierarchy, a new instance is created. This is important to consider when designing: if you need to preserve data between view removals, use a service layer (singleton or DI) or @AppStorage for persistence.

swift
class TimerViewModel: ObservableObject {
    @Published var seconds: Int = 0
    private var timer: Timer?

    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            self.seconds += 1
        }
    }

    deinit {
        timer?.invalidate()
    }
}

struct TimerView: View {
    @StateObject var viewModel = TimerViewModel()

    var body: some View {
        Text("\(viewModel.seconds)s")
            .onAppear { viewModel.start() }
    }
}

In the example, TimerViewModel is created via @StateObject and lives as long as TimerView is on screen. The timer starts in onAppear and stops in deinit. If @ObservedObject were used, each re-render of TimerView would create a new TimerViewModel with seconds = 0, and the timer would never work correctly. @StateObject guarantees that the viewModel is unique and stable.

@StateObject vs @ObservedObject: Comparison

The choice between @StateObject and @ObservedObject depends on who owns the object. If the view creates the object — @StateObject. If the view receives a ready-made object — @ObservedObject. This rule is so important that Xcode shows a warning when using @StateObject in a child view that receives the object via an initializer.

SituationRecommended Wrapper
View creates model via ViewModel()@StateObject
View receives model from parent@ObservedObject
Model is used in one view@StateObject
Model is passed via Environment@EnvironmentObject
Model is needed for previews@ObservedObject + mock

In practice, at the start of a project, @StateObject is often used in the root view and @ObservedObject in all child views. As the application grows, some @StateObject instances can be replaced with @EnvironmentObject to simplify the hierarchy. However, @StateObject remains the best choice for modular screens with their own logic.

@StateObject Usage Patterns

The first pattern — MVVM with @StateObject. ViewModel as ObservableObject is created in the view via @StateObject. ViewModel contains @Published properties and business logic. The view subscribes to changes and updates the interface. This approach provides testable isolation: ViewModel can be tested without UI by creating an instance directly.

The second pattern — @StateObject with dependencies. If the ViewModel requires services, use initialization with parameters. For example, @StateObject var viewModel = UserViewModel(api: APIClient.shared). However, be careful: parameters are computed on every body re-render, but the object is created only once. SwiftUI ignores subsequent @StateObject initializations.

The third pattern — nested @StateObjects. In SwiftUI, you can have multiple @StateObjects in one view, but this is rarely justified. Usually one @StateObject handles the entire view’s data set. If the logic becomes too complex, break it into a composition of @ObservedObject services inside one @StateObject.

swift
struct AppView: View {
    @StateObject var router = NavigationRouter()
    @StateObject var auth = AuthViewModel()

    var body: some View {
        ContentView()
            .environmentObject(router)
            .environmentObject(auth)
    }
}

In the example, AppView creates two @StateObjects: NavigationRouter for managing navigation and AuthViewModel for authentication. Both objects are injected into the Environment via environmentObject. Any child view can access them through @EnvironmentObject without passing through the initializer chain.

@StateObject and Initialization with Parameters

@StateObject supports initialization with any parameters, but with an important caveat: the initializer is called only once. On subsequent body re-renders, new parameter values are ignored. This means that if you pass @State var id: Int = 5 into @StateObject var vm = ViewModel(id: id), when id changes, the ViewModel will not receive the new value.

To solve this problem, use onReceive or onAppear for synchronization. Subscribe to parameter changes inside the ViewModel via Combine or pass parameters through the .onChange(of:) method at the view level. An alternative is to use @ObservedObject instead of @StateObject if the object should dynamically respond to external changes.

swift
struct DetailView: View {
    let itemId: Int
    @StateObject var viewModel = DetailViewModel()

    var body: some View {
        Text(viewModel.title)
            .onAppear { viewModel.load(id: itemId) }
    }
}

The correct approach: DetailView receives itemId as a let property (passed through the structure’s initializer), and @StateObject creates DetailViewModel without parameters. In onAppear, the load(id:) method is called to load data for the passed ID. This guarantees that the ViewModel is created by the @StateObject mechanism, but data is loaded on each view appearance with the current ID.

Common @StateObject Mistakes

The main mistake — using @StateObject in child views that receive the object from a parent. If ParentView creates @StateObject model, and ChildView declares @StateObject var model: ModelType (with a default parameter), ChildView will create its own independent instance. The parent and child objects will not be connected, and changes in one will not reflect in the other.

The second mistake — placing @StateObject in List or ForEach. Each list element creates its own @StateObject, leading to multiple independent instances. For lists, the correct approach is to pass one ObservableObject to all elements via @ObservedObject or use Identifiable structures with @State inside List.

The third issue — lack of deinit cleanup. @StateObject lives for the entire view lifecycle. If the object creates timers, Combine subscriptions, or network requests, deinit must cancel them. Otherwise, memory leaks and continued background work after screen closure are inevitable. Always use a Combine Cancellable store or invalidate timers in deinit.

Frequently Asked Questions

When was @StateObject introduced in SwiftUI?

@StateObject was added in SwiftUI 2.0 at WWDC 2020 alongside iOS 14, macOS 11, watchOS 7, and tvOS 14. Before that, @ObservedObject was the only way to work with ObservableObject, which often led to data loss bugs.

Can @StateObject be optional?

No, @StateObject does not support Optional types. The object must be initialized at declaration. If you need an optional object, use @ObservedObject or @EnvironmentObject with an optional type.

How to verify that @StateObject is created only once?

Add print(#function) to the ObservableObject’s initializer and deinit. If init is not called on re-renders — @StateObject is working correctly. If init is called every time — replace @ObservedObject with @StateObject.

Can @StateObject be used with UIKit via UIHostingController?

Yes, @StateObject works in SwiftUI views embedded in UIKit via UIHostingController. The object lifecycle is tied to the SwiftUI view, not to the UIViewController. If the SwiftUI view is replaced, the @StateObject is destroyed.

What is better: one @StateObject with a large ViewModel or several small ones?

Several small @StateObjects with separated responsibilities. This improves testability, reusability, and performance — when one object changes, only subscribed parts of the interface are redrawn, not the entire view.

Summary

  • @StateObject — Property Wrapper for creating and owning ObservableObject in a view
  • Single instance — the object is not recreated on subsequent body re-renders
  • Source of truth — @StateObject in the root view ensures data stability for the hierarchy
  • Selection rule — @StateObject for creation, @ObservedObject for receiving a ready-made object
  • Initialization — parameters in @StateObject are computed once, updates are not tracked
  • Deinit — mandatory cleanup of timers and subscriptions in ObservableObject’s deinit
  • iOS 14+ — @StateObject is available starting from iOS 14, macOS 11, watchOS 7, tvOS 14

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also