Wondering if this is expected behaviour similar to how we cant set a different value within a variables willSet block. Is this due to race condition?
@MainActor
class Presenter {
var cancellables: Set<AnyCancellable> = .init()
@Published var state: State = .ready
init() {
$state.sink { [weak self] newValue in
guard let self else { return }
switch newValue {
case .loading:
switch state {
case .ready:
print("making this finish")
state = .finish // this will trigger sink block to run,
// but variable doesnt update.
default: break
}
default:
break
}
}
.store(in: &cancellables)
}
}
Task { @MainActor in
let presenter = Presenter()
presenter.state = .loading
print(presenter.state)
}
## prints ##
making this finish
loading
2
Answers
According to the documentation about @Published:
So, in your scenario, it makes sense because of the infinity loop. The
$state
cannot escape fromwillSet
.Yes, this is expected behaviour, and yes, this is very similar to the
willSet
situation.The
sink
closure will always be called before the property is actually set, as the documentation says.Let’s consider the state of the program each the
sink
closure is called.The first time it is called,
newValue
andstate
are both.ready
. This is the initial value, and there is nothing interesting here.The second time it is called,
newValue
is.loading
andstate
is.ready
. This call is caused by the linepresenter.state = .loading
. It is in this call thatstate = .finish
is run.This causes the
sink
closure to be called the third time, caused bystate = .finish
.newValue
is.finish
andstate
is still.ready
.state
is still not set to.loading
at this point because the second call to thesink
closure has not returned yet. If it had returned, it would have implied that the linestate = .finish
has been fully executed. This contradicts the fact that thesink
closure is always called before the value is set.If you put a breakpoint so that the program pauses at the third call of the
sink
closure, and dothread backtrace
in lldb, you can see that the second call is still on the call stack:Frame #17 is the setter setting
state
to.loading
. This eventually calls thesink
closure in frame #9, which in turn calls thestate
setter again (frame #8), and that calls thesink
closure again, in frame #0.Finally, each of these calls will return, popped off the stack. When the second call to the
sink
closure returns (frame #9),state
finally changes to.finish
. But eventually frame #17 returns, which setsstate
to.loading
.No. Everything is running on the
MainActor
here. There can’t be any races.