SwiftUI
StateObject
Swift programming
app development
iOS development

Initialize StateObject with a parameter in SwiftUI

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

@StateObject is the right property wrapper when a SwiftUI view owns the lifecycle of an observable reference type. Initializing it with runtime data is common, but the syntax is easy to get wrong if you try to assign directly in the property declaration. The correct pattern is to initialize the backing storage in the view initializer.

Why @StateObject Needs Special Initialization

@StateObject is created once per view identity, not on every body recomputation. That behavior protects your model from being recreated during state updates.

Because of that lifecycle guarantee, SwiftUI requires initialization through the underscored storage property in init, using StateObject(wrappedValue:).

Incorrect pattern:

swift
1struct DetailView: View {
2    let userId: String
3    @StateObject var vm = UserViewModel(userId: userId) // compile error
4
5    var body: some View {
6        Text(vm.title)
7    }
8}

Here, userId is not available when the property is initialized.

Correct pattern:

swift
1import SwiftUI
2
3final class UserViewModel: ObservableObject {
4    @Published var title: String = "Loading..."
5    private let userId: String
6
7    init(userId: String) {
8        self.userId = userId
9    }
10
11    func load() {
12        title = "User \(userId)"
13    }
14}
15
16struct DetailView: View {
17    @StateObject private var vm: UserViewModel
18
19    init(userId: String) {
20        _vm = StateObject(wrappedValue: UserViewModel(userId: userId))
21    }
22
23    var body: some View {
24        Text(vm.title)
25            .onAppear {
26                vm.load()
27            }
28    }
29}

Parent to Child Data Flow Patterns

When the parent creates and owns the model, pass it as @ObservedObject instead of @StateObject.

swift
1struct ParentView: View {
2    @StateObject private var vm = UserViewModel(userId: "42")
3
4    var body: some View {
5        ChildView(vm: vm)
6    }
7}
8
9struct ChildView: View {
10    @ObservedObject var vm: UserViewModel
11
12    var body: some View {
13        Text(vm.title)
14    }
15}

Ownership rule:

  • creator and lifecycle owner uses @StateObject
  • consumer uses @ObservedObject

Following this rule prevents accidental double initialization and confusing state resets.

Handling Parameter Changes Safely

A common misunderstanding is expecting @StateObject to recreate when input parameters change. It does not, unless the view identity changes.

If you need recreation for a different key, tie identity explicitly:

swift
1struct HostView: View {
2    @State private var selectedUserId = "42"
3
4    var body: some View {
5        DetailView(userId: selectedUserId)
6            .id(selectedUserId)
7    }
8}

Using .id tells SwiftUI this is a new view identity, so @StateObject is rebuilt.

If recreation is not desired, update the existing model instead by exposing an update method and calling it on change.

swift
1struct DetailView: View {
2    @StateObject private var vm: UserViewModel
3    let userId: String
4
5    init(userId: String) {
6        self.userId = userId
7        _vm = StateObject(wrappedValue: UserViewModel(userId: userId))
8    }
9
10    var body: some View {
11        Text(vm.title)
12            .onChange(of: userId) { newId in
13                vm.apply(userId: newId)
14            }
15    }
16}

That pattern avoids tearing down existing async tasks unless you choose to.

Async Loading Example with Cancellation

Many view models start async work. Keep cancellation inside the model to avoid stale updates.

swift
1import SwiftUI
2
3@MainActor
4final class ProductViewModel: ObservableObject {
5    @Published var name = "Loading..."
6    private var task: Task<Void, Never>?
7
8    func load(productId: String) {
9        task?.cancel()
10        task = Task {
11            try? await Task.sleep(nanoseconds: 300_000_000)
12            name = "Product \(productId)"
13        }
14    }
15
16    deinit {
17        task?.cancel()
18    }
19}
20
21struct ProductView: View {
22    @StateObject private var vm = ProductViewModel()
23    let productId: String
24
25    var body: some View {
26        Text(vm.name)
27            .task(id: productId) {
28                vm.load(productId: productId)
29            }
30    }
31}

This keeps the view simple and makes model behavior testable.

Testing Guidance

For unit tests, instantiate the view model directly and verify published state transitions. For UI tests, vary navigation paths and parameter changes to ensure model ownership remains stable.

A useful check is confirming the model is not re-created on trivial body updates. Logging object identity during development can reveal unintended resets.

Common Pitfalls

A frequent pitfall is using @ObservedObject when the view should own creation. The model may be recreated externally and produce inconsistent state.

Another issue is putting expensive network calls in the view initializer. Keep side effects in methods triggered by .task or .onAppear so lifecycle is explicit.

Developers also forget that changing a constructor argument does not automatically recreate @StateObject. Use .id only when full recreation is intentional.

Finally, avoid storing parent-owned dependencies inside child-owned models without a clear ownership boundary.

Summary

  • Initialize @StateObject with parameters via _property = StateObject(wrappedValue:) in init.
  • Use @StateObject for ownership and @ObservedObject for consumption.
  • Parameter changes do not recreate state objects unless view identity changes.
  • Prefer explicit update methods or .task(id:) for changing inputs.
  • Keep async cancellation and side-effect management inside the view model.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.