Alejandro Ciniglio

Wrapper view to unwrap observable objects

I have a list of ObservableObjects and an editing view that should let me edit them one at a time. I’m using an NSSplitViewController, so the list view and the editing view don’t share a SwiftUI ancestor.

I ended up using a PassthroughSubject to publish the selected item, and then the editing view has an onReceive on that publisher.

I ran into an issue with how to actually pass the fields of that item to SwiftUI views (e.g. TextField). To work around this, I made an intermediary wrapper view, that recieves the updated item, sets it to it’s own state, and then passes that as an @ObservedObject to the editor.

struct Wrapper: View {
    let selectedItem: PassthroughSubject<Item, Never>
    @State private var item: Item = Item.placeholder()
    
    var body: some View {
        EditorView(item: item)
            .onReceive(selectedItem) { newItem in
                item = newItem
            }
    }
}

Hashable and Equatable conformance for classes

If you just want instance comparison in swift, you can use these conformances.

class Foo: Hashable, Equatable, ObservableObject {
    static func == (lhs: Foo, rhs: Foo) -> Bool {
        lhs === rhs
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(self))
    }
}

Observable Object hierarchies in SwiftUI

Setting up the model and views

I’m starting to work on a new app, and the main piece I’m working on is allowing a user to enter their data for later viewing. The data is fairly structured, so this is essentially data modeling for CRUD operations.

The part of the app I’m working on now deals with workouts and a workout contains multiple exercises. Think “chest workout” includes “bench press” and “push ups”.

My data model then looks like

    struct Exercise {
        let name: String
        let workout: Workout
    }
    
    class Workout: ObservableObject {
        @Published let name: String
        @Published let exercises: [Exercise]
    }

(Workout can’t be a struct because they mutually refer to each other; see here for other ways to solve this problem)

Then into my main view I pass a binding to Workout so that the user can edit it as needed.

The problem

One of my views wants to attach some data to the exercises, but I don’t want to put that into the Exercise struct since it’s only for UI purposes. This led me to build a view model (in the colloquial sense, not necessarily the formal sense), that would encapsulate the view specific information and the workout information.

class WorkoutEditorViewModel: ObservableObject {
    @Published var workout: Workout
    @Published var expanded: [Bool]
    
    init(workout: Workout, defaultExpanded: Bool = false) {
        self.workout = workout
        self.expanded = workout.exercises.map { _ in defaultExpanded }
    }
}

Then in my view, I can use this model to present editors for all exercises inline.

struct WorkoutEditor: View {
    @ObservedObject var workout: WorkoutEditorViewModel
    
    var body: some View {

        ForEach(workout.workout.exercises.indexed(), id: \.1) { index, ex in
            ExerciseDetails(expanded: self.$workout.expanded[index],
                            name: self.$workout.workout.exercises[index].name,
                            exercise: self.$workout.workout.exercises[index])
        }
    }
}

Here, I’m using an extension indexed() to create a list of tuples that I can use to render a view for each item in the list. The tuples are needed because ForEach doesn’t allow index iteration if you’re going to be adding items to the list, and we need the index to get a binding to a specific exercise from the view model.

This renders fine, but when editing an exercise, I would get a runtime error Fatal error: Duplicate keys of type 'Exercise' were found in a Dictionary. This error doesn’t have much google juice, and as you can see, I haven’t actually used Exercise in a dictionary.

Solution

It occured to me that the reason ForEach requires an id: argument is possibly to put the backing data into a dictionary for faster future lookup. This meant that my ForEach was iterating over exercises that were being changed from underneath it, which could only happen if my view wasn’t being updated when the exercise was changed.

The real source of the problem is my nested ObservableObjects. Even though WorkoutEditorViewModel used @Published when declaring the workout property, this doesn’t automatically subscribe to the objectWillChange publisher of the Workout class.

Fortunately, with a little combine, we can do this manually. The corrected view model looks like this:

class WorkoutEditorViewModel: ObservableObject {
    @Published var workout: Workout
    @Published var expanded: [Bool]
    
    var anyCancellable: AnyCancellable? = nil
    
    init(workout: Workout, defaultExpanded: Bool = false) {
        self.workout = workout
        self.expanded = workout.exercises.map { _ in defaultExpanded }
        anyCancellable = workout.objectWillChange.sink { _ in
            self.objectWillChange.send()
        }
    }
}

Now, any time the underlying workout is changed, the view model will notify it’s subscribers (i.e. views) that it has changed as well. This will trigger a view update, and prevent our dictionary keys from being changed in place.

Hopefully this helps someone else running into this cryptic duplicate keys error!

Timestamps for Core Data Models

Add to the generated model+CoreDataClass.swift:

    override public func awakeFromInsert() {
        super.awakeFromInsert()
        let now = Date()
        self.setPrimitiveValue(now, forKey: "cd_created_at")

        self.cd_created_at = now
    }
    
    override public func willSave() {
        super.willSave()
        if !self.isDeleted && self.changedValues()["cd_updated_at"] == nil && self.changedValues().count > 0{
            self.cd_updated_at = Date()
        }
    }

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
        }
    }
}
1 of 2 Next Page