0

It's a common mistake to expect that a @Published property's value has been updated when .sink() block is executed. In that case however, the property still has the old value, because .sink() is triggered by willSet (as is explained here).

Some suggest, for example here, that adding .receive(on:) solves this issue.

But, I've also read somewhere, that adding .receive(on:), is not a fundamental fix. This makes me think that it may still fail under certain conditions.

So, my question is: Does adding .receive(on:) guarantee that the .sink() block will be executed after the property's value has actually been set (and didSet has been called)?

Here's some code from referenced example video above. Without the .receive(on:), the table does not show the new ["One", "Two", "Three"] content after addItems() is called; although the .sink() block is executed.

override func viewDidLoad()
{
    super.viewDidLoad()

    viewModel.$dataSource
        .receive(on: RunLoop.main) <<=== 'fixes' issue
        .sink(receiveValue:
        { [weak self] _ in
            self?.tableView.reloadData()
        }) 
} 

@IBAction func addItems()
{
    viewModel.dataSource = ["One", "Two", "Three"]
}

As an encore, here's a sightly related answer from @robmayoff about which scheduler to use; another commonly misunderstood subject.

meaning-matters
  • 21,929
  • 10
  • 82
  • 142
  • Actually, there is no issue here when the change is performed on the same thread (here the main thread). You can always define an update function like `update(oldValue:newValue:)`. This is also especially useful when you want to perform an animation based on the diff of new and old value. – CouchDeveloper Dec 28 '22 at 21:16

1 Answers1

3

Whenever you are dealing with asynchronous code, the word "guarantee" gets a bit dicy. When you use receive(on:) you are basically saying that at some unspecified point in the future, makes sure this message gets to the subscriber (in this case the sink block) in a particular context.

You've got three execution contexts to consider. The context publishing to the pipeline, the context setting a new value, and the context interested in reading the value once it's set.

receive(on:) could fail if the context publishing to the pipeline and the context reading the value are different from the context setting the value. In your example, the context setting the value is the "main context". If some thread other than the main thread writes to the property, the receive(on:) will schedule a "set" operation on the main thread. You will have no guarantees about when that set might actually occur.

Scott Thompson
  • 22,629
  • 4
  • 32
  • 34
  • Can you explain why adding the `.receive(on:)` does/might 'solve' the issue here? – meaning-matters Dec 28 '22 at 20:42
  • 1
    Suppose you set a new value to the published property *from the main thread*. The pipeline runs in `willSet`. When the pipeline hits the `receive(on:)`, rather than handling the value immediately, it schedules code the main thread later. The main thread continues to do the "set" before giving up control. At some point later the code of the pipeline that was scheduled to the main thread will be scheduled and can execute the blocks in the `sink`. It works because the `willSet` and "set" happen synchronously while the `sink` is delayed on the same thread. – Scott Thompson Dec 29 '22 at 15:43