I'm seeing some strange behavior around the synthesized objectWillChange when trying to access it manually, and I'm curious if I'm seeing a bug or if there's a good explanation for why it works this way.
The following ViewModel compiles (i.e. objectWillChange is synthesized), but if I bind it to a view using @ObservedObject the view is not updated as the names cycle:
class ViewModel: ObservableObject {
var name: String = "John" {
willSet {
objectWillChange.send()
}
}
private let names = ["John", "Fred", "Jake"]
init() {
cycleNames()
}
func cycleNames(startAt index: Int = 0) {
name = names[index]
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in
guard let self = self else { return }
let next = index + 1
if next == self.names.endIndex {
self.cycleNames(startAt: 0)
} else {
self.cycleNames(startAt: next)
}
}
}
}
If I manually add let objectWillChange = ObservableObjectPublisher() in ViewModel, the view updates properly as the names cycle.
To make matters more confusing, if I don't include the manual objectWillChange but do include a separate published property e.g. @Published var age: Int = 1, then the view updates properly as the names cycle.
So it looks like manually calling send() on a synthesized objectWillChange doesn't work unless there is at least one @Published property. Is this intended behavior or a bug?
The synthesized objectWillChange creates an ObservableObjectPublisher once and installs it into all the @Published properties using runtime introspection. When you access objectWillChange, it will take out the installed instance and return it to you.
Since there's no @Published properties in your class, there's no storage objectWillChange could install the publisher into, so it just creates and returns a new instance every time you access objectWillChange.
So in your case you should either make your name property @Published, or you provide a custom implementation of the objectWillChange property (trivially, by adding a stored constant to your ViewModel class)
Seeing your implementation is really helpful! I had assumed that objectWillChange was statically synthesized with some kind of compiler magic.
So if I understand correctly, Published has space for the objectWillChange reference and the same ObservableObjectPublisher is put into every published property when objectWillChange is accessed?
It was definitely mystifying to me that objectWillUpdate existed but my view wasn’t updating, though it makes sense once you understand the implementation. I wonder if making this an assertion failure in debug builds with a helpful message could help prevent confusion.
It's still very likely not what you want, to have new objectWillChange every time. It's still that the synthesis is not doing a good job deciding a default behaviour.
@anandabits Funny that I independently filed mine last month as well (FB7321036). With no update either.
Sorry for resurrecting this old thread, but it seems like this behaviour has changed recently, perhaps with Swift 5.2?
Certainly with Xcode 11.5 and Swift 5.2, not instantiating your own ObservableObjectPublisher now works even if you don't have any @Published properties.