I've read the docs multiple times and need clarification...
Given the snippet below:
let signal: Signal<Value,Error>
//call this observer y
signal.take(first: 1).observeValues{ (value) in
//intended strong capture on self. this is the only one that retains self so if this observer is triggered and completes, self should dealloc
self.doSomethingElse(value) //trivial call, no async or thread hopping
}
//call this observer x
signal.take(duringLifetimeOf: self).observeValues{ [unowned self] (value) in //is this safe or better to use weak and guard against it?
self.doSomeProcess(value) //trivial call, no async or thread hopping
}
If signal
is triggered and notifying its observers for value event
:
1) Observer y
would be notified before x
(assumption because it is first observed so earlier in queue)
2) Since y
would complete after processing the value, self
should dealloc afterwards
Question:
Which events would x
receive (in order):
value and completion? Is it guaranteed that self would still be alive while the value event is processed?
completion only? I doubt this is the case but kindly refer some doc if so. Because completion events aren't propagated immediately.
Would using different Scheduler
s for both x
and y
would make an effect on the outcome?
Lastly, am I introducing race
? I doubt this because reactiveSwift does not introduce concurrency unless explicitly stated by the dev.
I put together a little sample console app to test this. As I said in my comment, take(first: 1)
delivers the completion event synchronously immediately after passing along the 1 value event, which means y
s reference to self
will go away before any values are delivered to x
. Assuming that's the only strong reference to self
, x
won't receive any values.
import Foundation
import ReactiveSwift
import ReactiveCocoa
class MyClass {
init(signal: Signal<String, Never>) {
//call this observer y
signal.take(first: 1).observeValues{ (value) in
//intended strong capture on self. this is the only one that retains self so if this observer is triggered and completes, self should dealloc
self.doSomethingElse(value) //trivial call, no async or thread hopping
}
//call this observer x
signal.take(duringLifetimeOf: self).observeValues{ [unowned self] (value) in //is this safe or better to use weak and guard against it?
self.doSomeProcess(value) //trivial call, no async or thread hopping
}
}
func doSomethingElse(_ value: String) {
print("Something Else: \(value)")
}
func doSomeProcess(_ value: String) {
print("Some Process: \(value)")
}
}
let (signal, input) = Signal<String, Never>.pipe()
_ = MyClass(signal: signal)
input.send(value: "1")
input.send(value: "2")
Sure enough, doSomeProcess
is never called:
Something Else: 1
Program ended with exit code: 0
The key thing to remember about ReactiveSwift
, is that everything happens synchronously unless you explicitly specify otherwise with a specific set of operators or with your own code. So the take
operator doesn't send along the one value event and then somehow "schedule" the delivery of the completion event for later. Both the value and completion event delivery happens during the upstream signal's delivery of the value event, and the deallocation of the observer and its references happens before signal
has finished delivering its first event.
When you say "completion events aren't propagated immediately" I assume you are talking about the portion of the APIContracts
file that talks about how failures and interruptions are propagated immediately. This is simply noting that many operators pass these events on immediately even if they are asynchronous or time-shifting operators.
The take
operator is not a time-shifting or asynchronous operator. And in this case, the operator is not propagating a completion event from the upstream signal; rather, it is generating the completion event itself, and it is doing so synchronously immediately after it propagates the value event.
You are correct that ReactiveSwift
does not introduce asynchrony or concurrency on its own, so there is no "race" here in the traditional sense. However, I believe the API contract for Signal
does not guarantee that events are delivered to observers in the order they started observing. So this code's behavior is technically undefined and could change in future versions of ReactiveSwift
.
Now this actually would introduce a race because take
's completion event would be delivered on whatever scheduler you set up for that observer, and that event's delivery would trigger the deinit
of self
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With