[SC]()

iOS. Apple. Indies. Plus Things.

Observable Structs in SwiftUI

// Written by Jordan Morgan // Feb 21st, 2022 // Read it in about 2 minutes // RE: Tech Notes

This post is brought to you by Emerge Tools, the best way to build on mobile.

Structs, am I right?

In the Swift community, we sometimes tend to lift up the little guys as a silver bullet. Alas, they have a time and place like anything else. It just happens, that time seems to be….like, all the time, particularly when it deals with SwiftUI.

Which is fine, but I ran into a unique obstacle the other day while working on my next indie project1. I’m leveraging Struct models throughout, and all is well until I actually need reference semantics. Picture this: You’re on iPadOS, using multitasking with the same app. I wanted data changed in both windows to update together.

Take this simple app:

struct Person {
    var name: String
    var age: Int
}

struct ContentView: View {
    @State private var person: Person
    
    init(person:Person) {
        _person = State(initialValue: person)
    }
    
    var body: some View {
        Form {
            Section("Edit Name") {
                TextEditor(text: $person.name)
            }
            Section("Age:") {
                Stepper("\(person.age)", onIncrement: { person.age += 1 }, onDecrement: { person.age -= 1})
            }
        }
    }
}

State is intended to drive only the view it’s used within, as is evident by the fact that they should be marked as private. As such, changing the name in Window B won’t update Window A:

A name being edited in two windows on iPadOS, and only one is updating.

So, we’ve got a value type that I want to act like a reference type. Enter either ObservableObject or the more nascent StateObject. Refactoring my model layer to use Class instances isn’t happening, so instead I made a generic class to wrap a Struct, thus gaining reference type semantics:

class WrappedStruct<T>: ObservableObject {
    @Published var item: T
    
    init(withItem item:T) {
        self.item = item
    }
}

And, I deploy it like so:

struct ContentView: View {
    @ObservedObject private var person: WrappedStruct<Person>
    
    init(person:Person) {
        _person = ObservedObject(wrappedValue: WrappedStruct(withItem: person))
    }
    
    var body: some View {
        Form {
            Section("Edit Name") {
                TextEditor(text: $person.item.name)
            }
            Section("Age:") {
                Stepper("\(person.item.age)", onIncrement: { person.item.age += 1 }, onDecrement: { person.item.age -= 1})
            }
        }
    }
}

And voilà:

A name being edited in two windows on iPadOS, and both are updating.

Drawbacks? It’s a little more ceremonial to use. For example, referencing the variable looks like this:

$person.item.name // Where item is really the Person

Instead of:

$person.name

Update: A few folks reached out after reading and let me know that you circumvent this particular issue using @dynamicMemberLookup, more info here.

You might also be wondering why I didn’t use state object. The short answer is, it doesn’t work for this use case. The longer answer, I suspect, has to do with how state object manages its lifecycle. I believe they are kept around even when the View they belong to are updated.

Anyways, if you need your value types to reference type, this is how I’ve done it.

Until next time ✌️.

  1. That’s right, this is a Tech Note baby. The blog posts I write about fun programming bits in my own person projects. It feels good to be back! 

···

Spot an issue, anything to add?

Reach Out.