For a beginner in the FRP paradigm, throttling is a foreign concept. A quick way to understand it in the real world is via a search. Let's imagine that you are searching something and everytime you tap the keyboard a new server is initiated. This can be quite expensive and complex to implement. So:
- Character "A" is inputted.
- New server call starts.
- Character "B" is inputted.
- Cancel the current on going server call and initiate a new one
The problem with this approach is that it doesn't take into account the frequency at which a new letter is inputted. Not only that, it's a waste of resources.
Throttling solves this problem by creating an interval between each input. Imagine that the user is inputting a new letter every 0.1s. If we set throttling with an interval of 0.5s, only at the 5th letter the search will commence. This makes more sense than going back and forth with new requests and cancelling the previous one.
In our example (that can be found here), we make use of the throttle
operator to achieve what was described above. Let's start with the SearchViewController
:
viewModel.result.producer.observeOn(QueueScheduler.mainQueueScheduler).startWithNext {[weak self] _ in
self?.tableView.reloadData()
}
In this case we are just reloading the UITableView
every time a new set of results comes. As a side note, RxSwift provides a more integrate approach to this kind of problems.
In the second bit, we are setting the new search term:
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
viewModel.searchText.value = searchText
}
Let's now have a look at the SearchViewModel
, more specifically, to how we are building the data source:
static private func generateDataSource() -> SignalProducer<[String], NoError> {
return SignalProducer {observable, disposable in
let path: String = NSBundle.mainBundle().pathForResource("words", ofType: "txt")! // 1
let string: String = try! String(contentsOfFile: path, encoding: NSUTF8StringEncoding) // 2
let words = string.characters.split("\n").map(String.init) // 3
observable.sendNext(words) // 4
observable.sendCompleted() // 5
}
}
The weird part is how a SignalProducer
is created, the rest is pretty standard:
- Get the path to the
words.txt
file that's located in our main bundle. - Create a string with the file's content.
- Split them into an array of words.
So the SignalProducer
has you have guessed, is a struct that takes a closure to be initialized. The closure itself is of type (Signal<Value, Error>.Observer, CompositeDisposable) -> ())
. So what the hell does this means to you?
You can think of the first parameter, the observable
, as the entity that will control the flow of the SignalProducer
. As you can see on comment 4
, we are sending the words array down the stream and on line 5
complete it. If there was a case where we would handle an error, we would just:
observable.sendFailure(error)
The second parameter is an instance conforming to the Disposable
protocol. From the documentation:
When starting a signal producer, a disposable will be returned. This disposable can be used by the caller to cancel the work that has been started (e.g. background processing, network requests, etc.), clean up all temporary resources, then send a final Interrupted event upon the particular signal that was created.
In our example, we don't really use it, since once the reading starts, we can't really interrupt it. A good use case would be this.
Let's now go for the fun part the SearchViewModel
initializer:
init() {
let scheduler = QueueScheduler(name: "search.backgroundQueue") // 1
let dataSourceGenerator = SearchViewModel.generateDataSource().startOn(scheduler) // 2
let producer = combineLatest(searchText.producer, dataSourceGenerator) // 3
.throttle(0.3, onScheduler: scheduler) // 4
.map(SearchViewModel.wordsSubSet) // 5
// 6
result = AnyProperty(initialValue: [], producer: producer) // 7
}
Let's go line by line:
- A new
QueueScheduler
is created. A scheduler is a serial execution queue, where the work will be processed. In our case this will serve as a background queue. - We define that our data source creation, when it starts, should be done in the previously defined scheduler. The generation hasn't started yet.
- We combine the
searchText.producer
with our data source. What this means is: we will move to step 4 and 5, once they both sent at least one value. This is useful because, we will wait until the data source has been calculated. ThesearchText
by default will already have a value, since we have defined it asMutableProperty("")
(being the""
its first value). So if the user hasn't searched for anything yet, the functionSearchViewModel.wordsSubSet
will get as input("", ["a", "lot", "of", "words"]) // (String, [String])
. - Finally the reason for this example: the (
throttle
). If you haven't skim any text, this should be fairly easy to understand:throttle
will make sure a given amount of time passes (in our case 0.3s) before sending a new value. If multiple values were sent, it will go with last one. Bottom line: if the user is a quick typer, we will only process her input every 0.3s. ✨ - Finally, we just pass the
SearchViewModel.wordsSubSet
to themap
function, so we can find which words match the user's search. Themap
transformation will be in the form of(String, [String]) -> [String]
. - None of what I described so far has started!
- We initialize our
result
. The initial value is just an empty array ([]
), since we don't have anything ready. The second bit, is all our efforts from step 1 to 5. Every time a new array comes (a filtered array based on the search), theresult.value
is updated.
Ok, so where does this all starts? With the AnyProperty
's initilization. We then observe all these changes in the SearchViewController
and call reloadData()
.
Note: The next part is just a downward spiral into madness. If I have missed something, or I am completely wrong, please let me know!
We start by checking the AnyProperty
initializer:
public init(initialValue: Value, producer: SignalProducer<Value, NoError>) {
let mutableProperty = MutableProperty(initialValue)
mutableProperty <~ producer
self.init(mutableProperty)
}
Let's have a look at that innocuous <~
:
public func <~ <P: MutablePropertyType>(property: P, producer: SignalProducer<P.Value, NoError>) -> Disposable {
var disposable: Disposable!
producer.startWithSignal { signal, signalDisposable in
property <~ signal
disposable = signalDisposable
property.producer.startWithCompleted {
signalDisposable.dispose()
}
}
return disposable
}
And finally (I promise), the startWithSignal
:
public func startWithSignal(@noescape setUp: (Signal<Value, Error>, Disposable) -> ()) {
let (signal, observer) = Signal<Value, Error>.pipe()
// Disposes of the work associated with the SignalProducer and any
// upstream producers.
let producerDisposable = CompositeDisposable()
// Directly disposed of when start() or startWithSignal() is disposed.
let cancelDisposable = ActionDisposable {
observer.sendInterrupted()
producerDisposable.dispose()
}
setUp(signal, cancelDisposable)
if cancelDisposable.disposed {
return
}
let wrapperObserver: Signal<Value, Error>.Observer = Observer { event in
observer.action(event)
if event.isTerminating {
// Dispose only after notifying the Signal, so disposal
// logic is consistently the last thing to run.
producerDisposable.dispose()
}
}
startHandler(wrapperObserver, producerDisposable)
}
A couple of things here:
- Most of the work being done is related to cleanup and making sure everything is disposed correctly.
- We hook up our handler (the
setUp
, which is coming from the<~
implementation).- The property update via the
property <~ signal
, which pretty much just updates theproperty.value
when a new value comes (via Next). - The
AnyProperty
internally makes use of aMutableProperty
to do the heavy lifting.
- The property update via the
- We finally start the work associated with our initial search + data source generation in the
startHandler
.
Since RAC is something I am using everyday I find these explorations very useful. Thanks to Nacho Soto for the explanation!