Conquering ReactiveSwift: SignalProducer (Part 4)
Welcome to part 4 of my Conquering ReactiveSwift series. In the previous article, we learned how to create and observe a signal. In this article, we will introduce the concept of a SignalProducer, which is an important primitive under the category Source.
Definition
As the name implies, a SignalProducer is something that produces a Signal. Basically, a SignalProducer encapsulates a deferred and repeatable work which when started generates a Signal.
Well, how is it useful?
Remember, in our last article, we worked on the following problem statement.
Print a message of time elapsed on every five seconds interval.
We created a signal which emits an integer on every 5 seconds for next 50 seconds. Then we observed those integers and printed the time elapsed. Let’s suppose, now we want this to start on a button tap. However, as an observer, we can only observe the signal, we can’t make it start or stop. For this kind of scenario, a SignalProducer is a good fit.
So let’s get started!
We will encapsulate the integer emitting code in a SignalProducer.
//Creating a SignalProducerlet signalProducer: SignalProducer<Int, NoError> =
SignalProducer { (observer, lifetime) in for i in 0..<10 {
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0 * Double(i)) {
observer.send(value: i)
if i == 9 { //Mark completion on 9th iteration
observer.sendCompleted()
}
}
}
}
Here, A SignalProducer is initialized with a closure that is executed when start
method of the SignalProducer is invoked. This closure accepts an observer of type Signal.Observer<Int, NoError>
and a lifetime of type Lifetime
. The observer is used to send values. The lifetime gives us an opportunity to cancel ongoing work if the observation is stopped.
Now we have a SignalProducer ready. Let’s start and observe it.
//Creating an observerlet signalObserver = Signal<Int, NoError>.Observer (
value: { value in
print("Time elapsed = \(value)")
}, completed: {
print("completed")
}, interrupted: {
print("interrupted")
})//Start a SignalProducersignalProducer.start(signalObserver)
Here we must keep in mind that each invocation of a SignalProducer will produce different signals. The order and value of events received by observers of different invocations of a SignalProducer are also different.
Suppose we want to interrupt the SignalProducer after 10 seconds. To do so, we have to dispose of it after 10 seconds.
//Start a SignalProducerlet disposable = signalProducer.start(signalObserver)//Dispose after 10 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
disposable.dispose()
}
According to our current implementation, even though the observer is disposed after 10 seconds the SignalProducer keeps on emitting the integers for 50 seconds. When constructing a SignalProducer, it’s important to free up resources and stop ongoing tasks if it is interrupted. So let’s get that fixed.
let signalProducer: SignalProducer<Int, NoError> =
SignalProducer { (observer, lifetime) in
for i in 0..<10 {
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0 * Double(i)) {
guard !lifetime.hasEnded else {
observer.sendInterrupted()
return
}
observer.send(value: i)
if i == 9 {
observer.sendCompleted()
}
}
}
}
We will check hasEnded
property of lifetime and send an interruption event as sendInterrupted
.
Signal vs SignalProducer
To understand the difference between Signal and SignalProducer, let’s take the analogy of TV and on-demand streaming service. Signal behaves like a TV feed which is a continuous stream of video and audio. At a given point of time, every observer of the TV feed sees the same sequence of the frame. The observer neither can inject any side effect to TV feed, nor it can start or stop the feed. The observer can only start and stop receiving the feed. On the other hand, SignalProducer is like an on demand streaming service like YouTube. Here, the observer can receive a stream of video and audio, but the sequence of the stream is different for a different observer. Here the observer can start and stop the feed.
Therefore, Signals are generally used to represent event streams that are already “in progress,” such as notifications, user input, etc. SignalProducers, on the other hand, are used to represent operations or tasks which need to be started. For example, like network requests, where each invocation of start
will create a new underlying operation. In the case of a Signal, the results might be sent before any observers are attached. In the case of a SignalProducer, the results are not sent unless it is started.
Conclusion
I hope this article gives you an idea about when to use a Signal and when use a SignalProducer, as well as a better understanding of the relationship between the two. You can find the sample code for this article here. In the next article, we will discuss how to limit the scope of observation.
Thank you for reading :)
Here is the complete list of articles you may want to follow:
- Introduction to ReactiveSwift (Part 1)
- Conquering ReactiveSwift: Primitives (Part 2)
- Conquering ReactiveSwift: Signal and Observer (Part 3)
- Conquering ReactiveSwift: SignalProducer (Part 4)
- Conquering ReactiveSwift: Property (Part 5)
- Conquering ReactiveSwift: Action (Part 6)
- Conquering ReactiveSwift: Disposable and Lifetime (Part 7)