Is this a bug in @Published?

I've run into this a lot recently. It's a problem when you have code that depends on multiple @Published properties (e.g. you're producing a string that describes an object in terms of its significant properties). The code is triggered when a property value changes, but it can't use the values of any of the observed object properties because one of them is un-updated at that point.

Instead, you have to cache all of the relevant properties, and construct the description from the cache, not the object. Depending on the properties, that can be quite expensive.

At least, I haven't found any other way around this except by caching property values.

2 Likes

I mean, if you're observing multiple @Published properties, using CombineLatest is usually what you want.

That only pushes the problem around, really. You still end up having to pass around multiple values into functions where you look at some properties and some in-flight values (or store the values to avoid passing them around).

This is not absolutely a show stopper, but it certainly constrains the way you can write code, without any really good reason beyond "Combine forces you to do it that way".

1 Like

True enough. I guess it just another form of the problem I described. I do wonder if this is just something missing from Combine (easy notification of change without value observation) or a mismatch with the philosophy behind stream programming.

Could I get some more info on how it's being used?

You mentioned that you get the old and new values when the publisher fires, but mostly I just discard the new value and trigger self.updateStuff() as in my previous post.

This is natural but dangerous for the reasons discussed in this thread:

self.subscriptions += self.state.$selection.sink { [weak self] _ in
	self?.updateSelection() // Whoops!
}

You can use dependency injection to avoid the issue:

self.subscriptions += self.state.$selection.sink { [weak self] selection in
	self?.update(selection: selection) // dependency injection starts out easy but...
	self?.update(selection: selection, b: self.state.b, c: self.state.c, d: ...) // ...it could get out of control
}
1 Like

Why does @Published published changes on willSet and not didSet in the first place? What is the advantage of this behavior?

See Tony’s latest post

As many of already said, one of the best answers is that it is simply and completely unexpected behavior.

Many use functions inside of a sink closure.

Like this:

@Published var errorWarning: String?
@Published var isWarningHidden = true

$errorWarning.sink { value in
    self.setup()
}

$isWarningHidden.sink { value in
    self.setup()
}

func setup() {
    label.text = errorWarning
    label.isHidden = isWarningHidden
}

Okay first off.. You can't do this. You can do this in other reactive libraries like RxSwift, and you see this exact same paradigm employed and encouraged in SwiftUI's body laying out the whole view at once when any @State property gets updated, but you can't do this here.

setup() would require two arguments. But $errorWarning.sink doesn't provide isWarningHidden and $isWarningHidden.sink doesn't provide errorWarning.

Fine then, you can go ahead and use CombineLatest.. but again you push this paradigm in SwiftUI. Users will start to think like this. Its not unreasonable for them to expect this to work like it does in the body of a SwiftUI View.

And also, can you even use more than 2 variables in CombineLatest? What if I add one more property?

@Published var errorWarningColor: UIColor?

func setup(errorWarning: String?, isWarningHidden: Bool) {
    label.text = errorWarning
    label.isHidden = isWarningHidden
    label.textColor = errorWarningColor
}

Oh bummer.. I broke it, but not intentionally. Why would I not be able to use errorWarningColor from the wrapped property? I can do so in SwiftUI. All this looks like to the writer is a bug.

2 Likes

Yes this is totally natural and an acceptable use case for using reactive code. SwiftUI bases the entire body paradigm around this functionality, but it only works for body. People are going to think like this when using Combine to crunch data, interface with UIKit, etc, but it only works in once place, SwiftUI, and appears "broken" everywhere else

If you don't need interoperability with SwiftUI, you can write your own @Published replacement pretty easily using OpenCombine's @Published implementation as a base, you just need to swap the order of the publishers in the linked section. Unfortunately such an implementation doesn't always work with SwiftUI. I really wish they would officially open that part.

People are going to think like this when using Combine to crunch data, interface with UIKit, etc, but it only works in once place, SwiftUI, and appears "broken" everywhere else

This is kind of the problem I had with even putting @Published (and ObservableObject) into Combine instead of leaving it in SwiftUI. That change would have made way more sense if @Published had exposed both didSet and willSet publishers.

Yep. Almost like instead of @Published(on: .didSet) we need something new. I know you can probably just make one, but I was trying to convince co-workers we could move away from RxSwift. This was the nail that ended that

This is the best solution I've seen. I feel weird about having to write @Published(on: didSet) and limiting the functionality of that publisher. Far better would be the ability to decide opportunistically whether or not you want your @Published to work for SwiftUI or for every other application

4 Likes

I agree, exposing a didSet Publisher off of the property wrapper that way seems like a decent solution.

@Published(didSet: true) is the answer.

Maybe, but wouldn't that force the author to either use a @Publisher for only SwiftUI, or only other purposes?

Forcing the behavior at declaration rather than when you sink as @timothycosta has proposed would force you to decide which, and you couldn't use both.

If you could decide on sink, then it wouldn't be that big of headache. One Publisher could be used in SwiftUI or for other UIKit,etc

1 Like

Definitely. I'm using SwiftUI and then sinking when I need to use UIKit. I think the best thing would be to replace @Published with something that publishes both. @BothPublished seems like a horrible name though.

I’ve just released the code I’ve been using to integrate ObservableObject classes with UIKit: GitHub - thoughtbot/CombineViewModel: An implementation of the Model-View-ViewModel (MVVM) pattern using Combine.. Instead of binding directly with each @Published Property, it provides the same update-coalescing behaviour of SwiftUI through a custom ObjectDidChangePublisher. You can use it directly like this:

myObject.observe(on: DispatchQueue.main).sink { object in
  // repeated willChange notifications have been coalesced here
}

You can also use the @ViewModel property wrapper to have the updateView() method automatically be called on a view controller whenever the object changes, just like how @ObservedObject works in SwiftUI.

2 Likes

I just ran into another issue where I'm relying on 6 properties to set my data. Some of them need to be published because they are tied to UI elements. Like search text, sort order, a segmented control, etc. Now I have to duplicate properties so SwiftUI can be in the know, but also so my filter() sort() functions can have access to the correct properties without passing them in as arguments on a CombineLatest (which doesn't support the amount of variables I want anyways)

Its really frustrating because this seems like such a basic headache encountered by basically everyone that tries to use this. Beginners to reactive frameworks are not going to understand what's going on at all, and even this forum will be difficult for them to grasp.

And then for those of us that understand Rx pretty well and are wanting to come over to combine.. we have to re-think the way we do things completely, and it still ends up not working.
The reason it doesn't work is basically unknowable to them. Almost like SwiftUI needs to automagically look for the willSet, and sink needs to automatically use didSet

is there any problem with adjusting the behavior in sink, and getting SwiftUI to still use willSet @Tony_Parker

2 Likes

Another example which is actually the thing that brought me here

@Published var names = ["Bryan", "Tony"]

$names.sink { _ in 
    self.tableView.reloadData()
}

This failed because the table view looked at the stored value

6 Likes