How to initialize @StateObject with parameters in SwiftUI

⋅ 4 min read ⋅ SwiftUI Data Property Wrapper

Table of Contents

Today I want to talk about something that I do a lot when creating a view in SwiftUI, but it is not obvious how to do it. That is, initialize @StateObject with parameters.

What is the proper way to initialize @StateObject with parameters

To understand the problem, I set up a DashboardView, which shows a greeting message to a logged-in user (User) using a view model DashboardViewModel. Here are the implementation details.

struct User {
let name: String // 1
}

class DashboardViewModel: ObservableObject {
@Published var greeting: String

init(name: String) {
greeting = "Hello, \(name)!" // 2
}
}

struct DashboardView: View {
var user: User // 3
@StateObject var viewModel = DashboardViewModel(name: user.name) // 4

var body: some View {
Text(viewModel.greeting) // 5
.padding()
}
}

1 A user model contains the name of the user.
2 DashboardViewModel accepts the name parameter and converts it to a greeting message.
3 DashboardView accepts user, <4> passes user's name to DashboardViewModel, and <5> showing greeting message in the text view.

Problem: property initializers run before 'self' is available

Xcode will show the following error, which is not a surprise. You can't use an instance property as an argument for another property.

Cannot use instance member 'user' within property initializer; property initializers run before 'self' is available

Cannot use instance member 'user' within property initializer; property initializers run before 'self' is available
Cannot use instance member 'user' within property initializer; property initializers run before 'self' is available

Problem: Lazy

It is tempting to use lazy, but that doesn't work either. You can't use lazy with a property wrapper.

@StateObject lazy var viewModel = DashboardViewModel(name: user.name)

Property 'viewModel' with a wrapper cannot also be lazy

Property 'viewModel' with a wrapper cannot also be lazy
Property 'viewModel' with a wrapper cannot also be lazy

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Solution

The solution for this problem needs three modifications in our view and view models.

Add a default value

First, we need to add a default value to our DashboardViewModel. So we can initialize it without passing parameters.

class DashboardViewModel: ObservableObject {
@Published var greeting: String

init(name: String? = nil) { // 1
if let name = name {
greeting = "Hello, \(name)!"
} else {
greeting = "Hello there"
}
}
}

1 We update our initializer to accept an optional string with a default value of nil.

With this change, we can initialize our view model without a problem.

struct DashboardView: View {
var user: User

@StateObject var viewModel = DashboardViewModel()

var body: some View {
Text(viewModel.greeting)
.padding()

}
}

But we need a way to update our view model to a passing user, which comes to the second change.

Expose a way to update the value

Since we no longer pass a name via the initializer, we need to expose another way to update it. I introduce a new update method in this case.

class DashboardViewModel: ObservableObject {
@Published var greeting: String

init(name: String? = nil) {
if let name = name {
greeting = "Hello, \(name)!"
} else {
greeting = "Hello there"
}
}

func update(name: String) { // 1
greeting = "Hello, \(name)!"
}
}

1 An update method that accepts a new name and updates the greeting message.

Update the value in onAppear

The only thing left is to update our view model with a user name. We do this in onAppear().

struct DashboardView: View {
var user: User

@StateObject var viewModel = DashboardViewModel()

var body: some View {
Text(viewModel.greeting)
.padding()
.onAppear {
viewModel.update(name: user.name) // 1
}

}
}

1 We update our view model as soon as the view appears.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Conclusion

Even I named this post a "Proper way to initialize @StateObject with parameters in SwiftUI", I'm not sure this is the best way or not. It is the only way I found so far[1]. I'm still new to SwiftUI and love to learn the better way to do this if you got one. You can direct message or tweet to me on Twitter.


  1. The closest reference I can get is in iOS App Dev with SwiftUI Tutorials where scrumTimer is reset every time the view appears. ↩︎


Read more article about SwiftUI, Data, Property Wrapper, or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
What is the difference between Tuist init and scaffold

A brief summary of init and scaffold commands.

Next
How to resize and position an image in UIImageView using contentMode

Learn thirteen ways to position and resize UIImage in UIImageView.

← Home