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
Wondering if this is expected behaviour similar to how we cant' set a different value within a variables
willSet
block.
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
and state
are both .ready
. This is the initial value, and there is nothing interesting here.
The second time it is called, newValue
is .loading
and state
is .ready
. This call is caused by the line presenter.state = .loading
. It is in this call that state = .finish
is run.
This causes the sink
closure to be called the third time, caused by state = .finish
. newValue
is .finish
and state
is still .ready
. state
is still not set to .loading
at this point because the second call to the sink
closure has not returned yet. If it had returned, it would have implied that the line state = .finish
has been fully executed. This contradicts the fact that the sink
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 do thread backtrace
in lldb, you can see that the second call is still on the call stack:
* frame #0: 0x00000001046602ec Foo`closure #1 in Presenter.init(newValue=finish, self=0x0000600000c0a820) at ContentView.swift:29:19
frame #1: 0x00000001b7de3208 Combine`Combine.Subscribers.Sink.receive(τ_0_0) -> Combine.Subscribers.Demand + 84
frame #2: 0x00000001b7de381c Combine`protocol witness for Combine.Subscriber.receive(τ_0_0.Input) -> Combine.Subscribers.Demand in conformance Combine.Subscribers.Sink<τ_0_0, τ_0_1> : Combine.Subscriber in Combine + 20
frame #3: 0x00000001b7dec610 Combine`Combine.PublishedSubject.Conduit.offer(τ_0_0) -> () + 428
frame #4: 0x00000001b7ded21c Combine`partial apply forwarder for closure #1 (Combine.ConduitBase<τ_0_0, Swift.Never>) -> () in Combine.PublishedSubject.send(τ_0_0) -> () + 40
frame #5: 0x00000001b7e23638 Combine`Combine.ConduitList.forEach((Combine.ConduitBase<τ_0_0, τ_0_1>) throws -> ()) throws -> () + 212
frame #6: 0x00000001b7dec19c Combine`Combine.PublishedSubject.send(τ_0_0) -> () + 196
frame #7: 0x00000001b7e0b040 Combine`static Combine.Published.subscript.setter : <τ_0_0 where τ_1_0: AnyObject>(_enclosingInstance: τ_1_0, wrapped: Swift.ReferenceWritableKeyPath<τ_1_0, τ_0_0>, storage: Swift.ReferenceWritableKeyPath<τ_1_0, Combine.Published<τ_0_0>>) -> τ_0_0 + 256
frame #8: 0x000000010465f940 Foo`Presenter.state.setter(value=finish, self=0x0000600000c0a820) at ContentView.swift:0
frame #9: 0x0000000104660468 Foo`closure #1 in Presenter.init(newValue=loading, self=0x0000600000c0a820) at ContentView.swift:35:27
frame #10: 0x00000001b7de3208 Combine`Combine.Subscribers.Sink.receive(τ_0_0) -> Combine.Subscribers.Demand + 84
frame #11: 0x00000001b7de381c Combine`protocol witness for Combine.Subscriber.receive(τ_0_0.Input) -> Combine.Subscribers.Demand in conformance Combine.Subscribers.Sink<τ_0_0, τ_0_1> : Combine.Subscriber in Combine + 20
frame #12: 0x00000001b7dec610 Combine`Combine.PublishedSubject.Conduit.offer(τ_0_0) -> () + 428
frame #13: 0x00000001b7ded21c Combine`partial apply forwarder for closure #1 (Combine.ConduitBase<τ_0_0, Swift.Never>) -> () in Combine.PublishedSubject.send(τ_0_0) -> () + 40
frame #14: 0x00000001b7e23638 Combine`Combine.ConduitList.forEach((Combine.ConduitBase<τ_0_0, τ_0_1>) throws -> ()) throws -> () + 212
frame #15: 0x00000001b7dec19c Combine`Combine.PublishedSubject.send(τ_0_0) -> () + 196
frame #16: 0x00000001b7e0b040 Combine`static Combine.Published.subscript.setter : <τ_0_0 where τ_1_0: AnyObject>(_enclosingInstance: τ_1_0, wrapped: Swift.ReferenceWritableKeyPath<τ_1_0, τ_0_0>, storage: Swift.ReferenceWritableKeyPath<τ_1_0, Combine.Published<τ_0_0>>) -> τ_0_0 + 256
frame #17: 0x000000010465f940 Foo`Presenter.state.setter(value=loading, self=0x0000600000c0a820) at ContentView.swift:0
Frame #17 is the setter setting state
to .loading
. This eventually calls the sink
closure in frame #9, which in turn calls the state
setter again (frame #8), and that calls the sink
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 sets state
to .loading
.
Is this due to race condition?
No. Everything is running on the MainActor
here. There can't be any races.