I have a ObservableObject
class that has a @Published
variable that is mutable. I have a function that mutates the value on the ObservableObject class a number of times. How could I collect the separate mutations into a transaction so that the objectWillChange publisher gets sent once?
For example:
struct MyDataModel {
var t1: String = ""
var t2: String = ""
var t3: String = ""
}
class MyOO: ObservableObject {
@Published var data: MyDataModel = MyDataModel()
func mutateData() { // collect all the mutations in this method
data.t1 = "1"
data.t2 = "12"
data.t3 = "123"
}
}
let myoo = MyOO()
_ = myoo.objectWillChange.sink { print(myoo.data) }
myoo.mutateData()
This code results in:
MyDataModel(t1: "", t2: "", t3: "")
MyDataModel(t1: "1", t2: "", t3: "")
MyDataModel(t1: "1", t2: "12", t3: "")
whereas I'd like:
MyDataModel(t1: "1", t2: "12", t3: "")
One obvious way is to copy the value of the published variable mutate it and set it back. But that requires significant code changes on my part and I hope there is a simpler/ idiomatic way to do this?
Two observations:
A key reason that SwiftUI uses objectWillChange
is so that it can coalesce multiple property updates into a single UI update. As they say in Data Essentials for SwiftUI:
Whenever you use
ObservedObject
, SwiftUI will subscribe to theobjectWillChange
of that specificObservableObject
. Now, whenever theObservableObject
will change, all the view that depends on it will be updated. One of the questions that we often get is “why ‘will’ change? Why not ‘did’ change?” And the reason is that SwiftUI needs to know when something is about to change so it can coalesce every change into a single update.
So, I understand the theoretical appeal of coalescing these events yourself, but one has to ask why go through that when SwiftUI already does this?
If I wanted to minimize events (e.g., if I were using myoo.$data.sink {…}
), I would write an extension on MyDataModel
with a mutating method that updated multiple properties as desired:
struct MyDataModel {
var t1: String = ""
var t2: String = ""
var t3: String = ""
}
extension MyDataModel {
mutating func update(t1: String, t2: String, t3: String) {
self.t1 = t1
self.t2 = t2
self.t3 = t3
}
}
And then:
class MyOO: ObservableObject {
@Published var data: MyDataModel = MyDataModel()
func mutateData() { // collect all the mutations in this method
data.update(t1: …, t2: …, t3: …)
}
}
That results in a single event.
This also encapsulates mutation in the MyDataModel
to that the model object. Personally, if I were dealing with a mutable object, I might ask whether it is appropriate for callers to even be allowed to mutate individual properties (especially if mutating one property and not another could temporarily result in an internally inconsistent state). So I might explicitly constrain mutation to the data model:
struct MyDataModel {
private(set) var t1: String = ""
private(set) var t2: String = ""
private(set) var t3: String = ""
}
// the rest is the same as above
That is up to you.
All of this having been said, the above would publish events as you go from MyDataModel(t1: "", t2: "", t3: "")
to MyDataModel(t1: "1", t2: "12", t3: "123")
. But you seem to be explicitly asking to see objectWillChange
that reflected two of the three property changes. It is unclear whether this aspect of the question was a key requirement, or whether you just wanted to coalesce the multiple property changes into a single event.
As Cy-4AH suggested (+1), we would often favor immutability, and just replace the object with a new instance. That would solve the “multiple event” problem. (It also affords the elimination of many races.) But we do not have enough information about the broader problem to say whether that is appropriate here or not.