Alejandro Ciniglio

An alternative to @ObservedObject for SwiftUI

Why Use ObservedObject

@ObservedObject (and its partner ObservableObject) is used in SwiftUI to allow us to encapsulate more significant logic than the simple state changes that @State is built for. In essence, @State is great if you’re changing a single value from something happening in the UI, but if you have an object that is changing or more complex logic, using a class that implements ObservableObject is the way to go.

With ObservableObject, you need to publish (@Published annotation on) at least one property which will create a combine publisher in your object that your view will subscribe to. (@ObservedObject is what wires this together, but you could do it directly with .onRecieve(yourObject.objectWillChange) in your view.)

This works great if there’s something in your class that you want to directly surface to the view and cause an update on that view whenever it changes.

The Problem

However, if you don’t have something that your SwiftUI view is directly relying on, you don’t have a property to put @Published onto.

I ran into this issue with a class that was collecting some information from a wrapped UIKit view, but my SwiftUI view only cared about a count. My model looked like


import Foundation

class SelectedThings {
    private struct ThingWithViewInfo {
        let thing: Thing
        let indexPath: IndexPath
    }
    
    private var myThings: Set<ThingWithviewinfo> = Set()
    
    public func addThing(_ thing: Thing, at indexPath: IndexPath) {
        myThings.insert(ThingWithViewInfo(thing: thing, indexPath: indexPath))
    }
    
    public var things : [Thing] { myThings.map(\.thing) }
}

(storing an indexpath like this doesn’t seem to be a great idea, but that’s for another day.)

and then my view that just wants to show the count to the user:

import SwiftUI

struct Content: View {
    let selectedThings: SelectedThings
    
    var body: some View {
        Text("\(selectedThings.things.count) things selected")
    }
}

My initial reaction was to only pass the count to the view and then mark things with @Published in my class. However, you can’t publish a computed property, and because my struct was private and I saw no great reason to expose it, publishing myThings was also not an option.

Manual Combine to The Rescue

I mentioned above that under the hood, @Published and @ObservedObject are wiring up a Combine publisher and subscriber to trigger the view to recalculate. Setting up a publisher and subscriber is something I’d done before, and I realized that I already knew a way to force the view to recalculate: by updating a @State variable.

Putting those together, I added a publisher to my class, and a new @State variable and onRecieve call to my view. When my private variable is updated, we cause the publisher to emit a value which the onRecieve gets and uses to update the state of the view, causing our view to update itself.

The class now looks like


import Foundation

class SelectedThings {
    private struct ThingWithViewInfo {
        let thing: Thing
        let indexPath: IndexPath
    }
    
    private var myThings: Set<ThingWithviewinfo> = Set()
    public var thingsChangedPublisher: PassthroughSubject<Void, Never> = PassthroughSubject()
    
    public func addThing(_ thing: Thing, at indexPath: IndexPath) {
        myThings.insert(ThingWithViewInfo(thing: thing, indexPath: indexPath))
        thingsChangedPublisher.send()
    }
    
    public var things : [Thing] { myThings.map(\.thing) }
}

and the view:

import SwiftUI

struct Content: View {
    let selectedThings: SelectedThings
    @State private var thingsCount = 0
    
    var body: some View {
        Text("\(thingsCount) things selected")
        .onRecieve(selectedThings.thingsChangedPublisher) {
            self.thingsCount = self.selectedThings.things.count
        }
    }
}