Hallo boeken en tijdschriften, Leo hier. Today we will talk about nested observables problem in SwiftUI.

As I continue to explore SwiftUI I’ve found some interesting behavior. One thing that is really different from UIKit for example, is that the order of the declarations of the view modifiers matters. In fact, the order is crucial to achieving the resulting view. It is totally different result when you apply “padding” before adding a “background” modifier and apply “padding” after adding the “background” modifier.

This is also true with animations. SwiftUI deprecated the “.animation()” because the behavior was totally unpredictable, sometimes it does what it suppose to do, and sometimes doesn’t.

These days I was thinking if SwiftUI is easy to learn and I didn’t get to a final answer. I think that creating an environment where you have a preview so that you can quickly see what you are building is really good for new people but on the other hand, SwiftUI touches a lot of complex development topics such as reactive programming and view rendering cycles.

Animations in SwiftUI are easy to implement, as demonstrated in this article where we create sound wave animations just with SwiftUI, and also how easy is to send data from UIKit to SwiftUI.

All in all, I think SwiftUI is a great tool and is not easy or hard, is just different than what we were used to doing. Nowadays, I feel that is a little faster writing views in SwiftUI than in UIKit but I never measured it so could just be a false impression.

Let’s code! But first…

 

The Painting of The Day

The painting I chose today is called The Education of the Children of Clovis, an 1861 painting by Sir Lawrence Alma-Tadema

One of late 19th-century Britain’s most well-known romantic painters is Lawrence Alma-Tadema. Laurens Tadema, a member of the town notary’s family, was his name at birth in the Netherlands.

Later, in an effort to carve out a place for himself in the art world, he spelled his first name “Lawrence” rather than the more English “Lord,” and he added the middle name “Alma” to his surname so that he would appear among the “A’s” in exhibition catalogues.

I chose this painting because the children is learning how to throw axes, and I’m learning how to navigate through new SwiftUI water each day.

 

The Problem

You want to use Nested Observables object in your SwiftUI View.

The nested ObservableObject problem in SwiftUI refers to a situation where an ObservableObject is used within another ObservableObject. In this case, the inner ObservableObject’s updates will not trigger a re-render of the outer ObservableObject’s views.

This causes a lot of questions such as:

“Why my View is not updating?”

“Why SwiftUI is not responding to the changes of the @StateObject or @ObservableObject?”

“What is the problem with my ObservableObjects that don’t trigger a SwiftUI View update?”

“Why is my SwiftUI view not updating when the model changes?”

Etc.

For example, imagine that you have a class called HomeData that is an ObservableObject and the HomeData has a @Published variable that is also an ObservableObject called UserData. If you make changes to the user data, and the view uses HomeData as the @StateObject you will not see the live changes.

This is because SwiftUI’s observation system only works on a single level of objects, and does not automatically propagate updates to nested objects.

Keeping that in mind, how can we solve that problem?

Let’s check the project below.

 

Set up SwiftUI Nested Observable Problem Example

Let’s suppose that you have a view that has a CustomerData ObservedObject and that object has CustomerStatus variable that is also an ObservableObject.

Check the example below:

class CustomerData: ObservableObject {
    let customerId = UUID().uuidString
    let customerName: String
    @Published var customerStatus = CustomerStatus() // nested observable problem!!!       
    @Published var timeStamp = Date()

    init(customerName: String) {
        self.customerName = customerName
    }

    func refreshStatus() {
        customerStatus.refreshStatus()
    }
    
    func refreshDate() {
        timeStamp = Date()
    }
}

class CustomerStatus: ObservableObject {
    var statusCode = 100
    @Published var lastUpdateCounter = 1

    func refreshStatus() {
        lastUpdateCounter += 1
    }
}

My naive thinking about this was: “Hey if SwiftUI can update the changes from the CustomerData with the refreshDate function it also can update the changes from the refreshStatus function”. Well… I was wrong.

Now let’s build a simple view to test the problem:

struct ContentView: View {
    @StateObject var customerData = CustomerData(customerName: "Mike")

    var body: some View {
        VStack(alignment: .leading) {

            VStack(alignment: .leading) {
                Text(customerData.customerName)
                    .font(.title)
                Text("\(customerData.timeStamp)")
                    .font(.subheadline)
                Button("Update Date") {
                    customerData.refreshDate() // this works
                }
                .padding(.bottom)
                .buttonStyle(.borderedProminent)

            }


            VStack(alignment: .leading) {
                Text("Customer Status: \(customerData.customerStatus.statusCode)")
                    .font(.title)
                    .foregroundColor(.white)

                Text("Last Updated Counter: \(customerData.customerStatus.lastUpdateCounter)")
                    .font(.subheadline)
                    .foregroundColor(.white)
                Button("Update Customer Status") {
                    customerData.refreshStatus() // This doesn't work
                }
                .buttonStyle(.borderedProminent)

            }
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 20)
                    .foregroundColor(.black)
            )

        }
        .padding()
    }
}

You should see this:

swiftui observableobject problem tutorial

Now if you tap the update Customer Status button, nothing will happen. However, when you tap the “Update Date” button, not only the date will be updated but also the Last Updated Counter.

That happens because when we just tap the Update Customer Status button, it doesn’t trigger the objectWillChange function from the CustomerData, only the one in the CustomerStatus.

Now that we have our code setup, let’s explore some answers to the not updating SwiftUI view with observable objects.

 

Solution 1 – Extract the Behavior to A View State Variable

With this solution, you don’t have to touch your observables code. What we will do is simply start to listen to the publisher from the @Published variable.

Check the code below:

struct ContentView: View {
    @StateObject var customerData = CustomerData(customerName: "Mike")
    @State var counter = 0 // create a new state to trigger the view redraw

    var body: some View {
        VStack(alignment: .leading) {

            VStack(alignment: .leading) {
                Text(customerData.customerName)
                    .font(.title)
                Text("\(customerData.timeStamp)")
                    .font(.subheadline)
                    .padding(.bottom)
            }


            VStack(alignment: .leading) {
                Text("Customer Status: \(customerData.customerStatus.statusCode)")
                    .font(.title)
                    .foregroundColor(.white)

                Text("Last Updated Counter:  \(counter)")
                    .font(.subheadline)
                    .foregroundColor(.white)
                    .onReceive(customerData.customerStatus.$lastUpdateCounter) { counter in // add this to the code 
                        self.counter = counter
                    }
                Button("Update Customer Status") {
                    customerData.refreshStatus()
                }
                .buttonStyle(.borderedProminent)

            }
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 20)
                    .foregroundColor(.black)
            )

        }
        .padding()
    }
}

Before you go and implement this, there are several problems with it. First is using the nested object directly, like “customerData.customerStatus.$lastUpdateCounter” is not desirable because of Demeter’s law, and also to this work properly you would have to add the initial state of the desired variable to the View also. A huge mess.

Let’s see if we can improve this.

 

Solution 2 – Call objectWillChange Manually when Update the Nested ObservableObject Data

This is also another way to solve this. You can manage yourself when to call the objectWillChange and SwiftUI will recreate the view for you. Meaning that every time you want to trigger a view update because of one of the nested observableObjects you need to remember to call the objectWillChange.send(). 

I don’t need to say that this is kinda wasteful, right? I don’t want to have to remember to call another function every time we add new functionality to the ObservableObjects. This for sure is a path to a lot of bugs and headaches in the future.

However, it is good to see this happening to learn more about how SwiftUI things work under the hood. Check below:

struct ContentView: View {
    @StateObject var customerData = CustomerData(customerName: "Mike")

    var body: some View {
        VStack(alignment: .leading) {

            VStack(alignment: .leading) {
                Text(customerData.customerName)
                    .font(.title)
                Text("\(customerData.timeStamp)")
                    .font(.subheadline)
                    .padding(.bottom)
            }


            VStack(alignment: .leading) {
                Text("Customer Status: \(customerData.customerStatus.statusCode)")
                    .font(.title)
                    .foregroundColor(.white)

                Text("Last Updated Counter: \(customerData.customerStatus.lastUpdateCounter)")
                    .font(.subheadline)
                    .foregroundColor(.white)
                Button("Update Customer Status") {
                    customerData.refreshStatus()
                }
                .buttonStyle(.borderedProminent)

            }
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 20)
                    .foregroundColor(.black)
            )

        }
        .padding()
    }
}

class CustomerData: ObservableObject {
    let customerId = UUID().uuidString
    let customerName: String
    @Published var customerStatus = CustomerStatus()
    @Published var timeStamp = Date()

    init(customerName: String) {
        self.customerName = customerName
    }

    func refreshStatus() {
        customerStatus.refreshStatus()
        objectWillChange.send() // calling here, will trigger the View Redraw.However, you have to add this line everywhere if you want to go with this solution.            
    }
}

class CustomerStatus: ObservableObject {
    var statusCode = 100
    @Published var lastUpdateCounter = 1

    func refreshStatus() {
        lastUpdateCounter += 1
    }
}

Of course, you could create a sink from the property of the nested ObservableObjects, but that also comes with the price you have to manage the sink connection yourself. If that breaks for some reason, you need to recreate it. There’s no easy way out of this, choosing this solution will be a headache eventually if the code grows.

Now let’s go to my recommended way.

 

Solution 3 – Extract The Nested ObservableObject

To me, this is the best solution. When you remove the dependency between the two, you solve the problem with Demeter’s law in the view using the property within the property and also make everything responsive without having to add any shady trick to it.

Check the recommended way to update the view and finish once and for all with this problem:

struct ContentView: View {
    @StateObject var customerData = CustomerData(customerName: "Mike")
    @StateObject var customerStatus = CustomerStatus()

    var body: some View {
        VStack(alignment: .leading) {

            VStack(alignment: .leading) {
                Text(customerData.customerName)
                    .font(.title)
                Text("\(customerData.timeStamp)")
                    .font(.subheadline)
                    .padding(.bottom)
            }


            VStack(alignment: .leading) {
                Text("Customer Status: \(customerStatus.statusCode)")
                    .font(.title)
                    .foregroundColor(.white)

                Text("Last Updated Counter: \(customerStatus.lastUpdateCounter)")
                    .font(.subheadline)
                    .foregroundColor(.white)
                Button("Update Customer Status") {
                    customerStatus.refreshStatus()
                }
                .buttonStyle(.borderedProminent)

            }
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 20)
                    .foregroundColor(.black)
            )

        }
        .padding()
    }
}

class CustomerData: ObservableObject {
    let customerId = UUID().uuidString
    let customerName: String
    @Published var timeStamp = Date()
    // no more nested observableObjects here

    init(customerName: String) {
        self.customerName = customerName
    }
}

class CustomerStatus: ObservableObject {
    var statusCode = 100
    @Published var lastUpdateCounter = 1

    func refreshStatus() {
        lastUpdateCounter += 1
    }
}

And with that, you have the cleanest way to solve your problems.

 

Wrap up on SwiftUI Views not Updating when Data Changes in Nested Observables

The nested ObservableObject problem in SwiftUI refers to a scenario where an ObservableObject is utilized within another ObservableObject, and in this case, the updates made to the inner ObservableObject will not result in the outer ObservableObject’s views being re-rendered.

The reason for this is that the observation system of SwiftUI only functions on a single level of objects and it doesn’t automatically propagate updates to any nested objects. This means that if you’re using an ObservableObject within another ObservableObject, the inner object’s updates will not trigger a re-render of the outer object’s views. This can be a problem because if the inner object’s state changes, the outer object’s views will not be updated to reflect those changes.

 

Summary – Nested Observables Problems in SwiftUI

Today we explore 3 solutions for this interesting problem in SwiftUI.

The first was binding the nested property to a View @State annotation, which would definitely trigger the View Redraw, but is not a good solution leaving the View entangled with the View data nested structure. The bright side of this approach is that this solution has zero effects on the Data layers, so if you don’t want to touch other layers’ code, this is one idea.

The second one was manually calling the objectWillChange.send(). This is also cumbersome because you need to remember to add the objectWillChange call every time you want to update the view. This is the receipt for bugs.

And lastly, we checked what is for me the best answer to this problem. If you can, remove the nested observed object and make two simple ObservedObjects. 

Fellow Apple Developers, that’s all. I hope you liked reading this article as much as I enjoyed writing it.  

If you want to support this blog you can Buy Me a Coffee or say hello on Twitter. I’m available on LinkedIn or send me an e-mail through the contact page.

You can likewise sponsor this blog so I can get my blog free of ad networks.

Thanks for the reading and… That’s all folks.

Image credit: Featured Painting