Dependency Injection with @State Variables
This post is brought to you by Emerge Tools, the best way to build on mobile.
Value types permeate my code base these days. Having been around this whole iPhoneOS SDK gig for a bit, I’ve spent enough evenings hunting down a bug caused by reference types. And so it is, in my SwiftUI endeavors I’ve inevitably hit situations where I need to pass a struct around, essentially - dependency injection between views when I’m outside of the normal pattern of an environment object of some sort.
Previously, I figured the answer to this scenario was to use one of @State
’s initializers - either providing an initial value or a wrapped one. Something like this:
struct PersonEditor: View {
@State private var person: Person
init(person: Person) {
_person = State(initialValue: person)
}
var body: some View {
Form {
Section("Details") {
TextField("Name", text: $person.name)
Stepper("Age: \(person.age)", value: $person.age, in:1...100, step: 1) { changed in
}
}
}
}
}
When discussing which of the two @State
initializers I should go with among a few dev friends, their answer surprised me:
One is bad, and the other is worse.
Well, okay then! Before we look at the easy peezy alternative, the reasons why it’s not recommended to go the route I have above are:
- Those state initializers can lead to unusual bugs that are tricky to pin down.
- By definition, if the struct is a piece of data the view doesn’t own, then
@State
is the wrong choice. - Generally speaking, if you find yourself needing to write a
View
’s initializer for something like this, you should try to figure out if there’s a way to model your data to avoid it. You’re kind of working against SwiftUI’s design at that point.
Leveraging Structs
Let’s revisit the example above. Let’s say to get to that view, we had a list showing before it - and tapping an item presented it:
struct DirectoryView: View {
@EnvironmentObject var store: Store
@State private var people: [Person] = []
var body: some View {
List(people) { person in
NavigationLink(presenting: person.id) {
PersonCellRow(idea)
}
}
.navigationDestination(for: Person.ID.self) { personID in
PersonEditor(people.person(for: personID))
}
.task {
people = store.getPeople()
}
}
}
So, for the PersonEditor
navigation destination, what we should do instead of initializing that @State
variable is simply switch it out to something SwiftUI expects for these flows, like a @Binding
:
struct PersonEditor: View {
@Binding var person: Person
var body: some View {
Form {
Section("Details") {
TextField("Name", text: $person.name)
Stepper("Age: \(person.age)", value: $person.age, in:1...100, step: 1) { changed in
}
}
}
}
}
And done. I could end the post here if I needed to.
But TL;DR - I typically boxed @Binding
properties into presentation contexts, or to modify one single property on another model. But when your model is a struct…then, a @Binding
is perfect! With the setup above, any edits we make to the model gets reflected back in the other list.
However, we can extend this pattern to make some U.X. wins very easily. The idea is this: Still pass in the binding, but also have another state variable of the model and edit that. You simply assign to it from the original binding. This way, if you want the edits to save - you simply assign the edited model back to the binding. But, if you don’t - just toss it away and dismiss the view. The binding variable remains untouched:
struct PersonEditor: View {
@Binding var person: Person
@State private var editedPerson: Person = .empty
@Environment(\.dismiss) private var dismiss
var body: some View {
Form {
Section("Details") {
TextField("Name", text: $editedPerson.name)
Stepper("Age: \(editedPerson.age)", value: $editedPerson.age, in:1...100, step: 1) { changed in
}
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
person = editedPerson
dismiss()
}
}
}
.onAppear {
editedPerson = person
}
}
}
I love this pattern. If you glance it over, maybe it’ll click for you as it did for me. To break down its advantages:
- If we don’t want to save the edits, we just leave the view. No harm done, since the binding (i.e.
person
here) remains untouched. Value types, yay! - If we do want to save our edits, well - now that’s as simple as assigning back to the
person
from oureditedPerson
struct. Done. Since it’s a binding, the view before becomes updated as well without much fuss. - Further, we can also apply easy U.X. wins, like disabling the
Save
button here if there were no edits made. That would just look like this:.disabled(editedPerson == Person.empty)
. I know that kind of thing isn’t exactly enabled because of this pattern, but it is a nice byproduct.
So, there you have it. I won’t come right out and proclaim this is the “right” way to wrangle dependency injection with @State
in SwiftUI, but I will say it’s objectively better than creating your state within your initializer. That’s fairly easy to point at and say, “That’s probably wrong.”
If anything, it’s a great thought process and another poignant reminder to always ask your other developer buddies how they solve stuff - it’s a wonderful way to learn.
Until next time ✌️