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 of 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. And no UI update happens.

So the question is – what needs to be done to cause the UI 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?

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()
	}
}

Thanks to Asperi on StackOverflow, the answer to this question is to now use objectWillChange.send() where the change is made (as opposed to didChange.send() as had previously been the suggested way.

1 Like