Ray
Ray

Reputation: 73

How to prod a SwiftUI view to update when a model class sub-property changes?

I've created a trivial project to try to understand this better. Code below.

I have a source of data (DataSource) which contains a @Published array of MyObject items. MyObject contains a single string. Pushing a button on the UI causes one of the MyObject instances to update immediately, plus sets off a timer to update a second one a few seconds later.

If MyObject is a struct, everything works as I imagine it should. But if MyObject is a class, then the refresh doesn't fire.

My expectation is that changing a struct's value causes an altered instance to be placed in the array, setting off the chain of updates. However, if MyObject is a class then changing the string within a reference type leaves the same instance in the array. Array doesn't realise there has been a change so doesn't mention this to my DataSource. No UI update happens.

So the question is – what needs to be done to cause the UI to update when the MyObject class's property changes? I've attempted to make MyObject an ObservableObject and throw in some didchange.send() instructions but all without success (I believe these are redundant now in any case).

Could anyone tell me if this is possible, and how the code below should be altered to enable this? And if anyone is tempted to ask why I don't just use a struct, the reason is because in my actual project I have already tried doing this. However I am using collections of data types which modify themselves in closures (parallel processing of each item in the collection) and other hoops to jump through. I tried re-writing them as structs but ran in to so many challenges.

import Foundation
import SwiftUI

struct ContentView: View
{
    @ObservedObject var source = DataSource()

    var body: some View
    {
        VStack
        {
            ForEach(0..<5)
            {i in
                HelloView(displayedString: self.source.results[i].label)
            }
            Button(action: {self.source.change()})
            {
                Text("Change me")
            }
        }
    }
}

struct HelloView: View
{
    var displayedString: String

    var body: some View
    {
        Text("\(displayedString)")
    }
}

class MyObject // Works if declared as a Struct
{
    init(label: String)
    {
        self.label = label
    }

    var label: String
}

class DataSource: ObservableObject
{
    @Published var results = [MyObject](repeating: MyObject(label: "test"), count: 5)

    func change()
    {
        print("I've changed")
        results[3].label = "sooner"
        _ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: {_ in self.results[1].label = "Or later"})
    }
}

struct ContentView_Previews: PreviewProvider
{
    static var previews: some View
    {
        ContentView()
    }
}

Upvotes: 3

Views: 2228

Answers (1)

Asperi
Asperi

Reputation: 258355

When MyObject is a class type the results contains references, so when you change property of any instance inside results the reference of that instance is not changed, so results is not changed, so nothing published and UI is not updated.

In such case the solution is to force publish explicitly when you perform any change of internal model

class DataSource: ObservableObject
{
    @Published var results = [MyObject](repeating: MyObject(label: "test"), count: 5)

    func change()
    {
        print("I've changed")
        results[3].label = "sooner"
        self.objectWillChange.send()    // << here !!

        _ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) {[weak self] _ in
            self?.results[1].label = "Or later"
            self?.objectWillChange.send()            // << here !!
        }
    }
}

Upvotes: 1

Related Questions