I'm using ReactiveCocoa to update a UILabel
whilst a UIProgressView
counts down:
NSInteger percentRemaining = ...;
self.progressView.progress = percentRemaining / 100.0;
__block NSInteger count = [self.count];
[[[RACSignal interval:0.05 onScheduler:[RACScheduler mainThreadScheduler]]
take: percentRemaining]
subscribeNext:^(id x) {
count++;
self.countLabel.text = [NSString stringWithFormat:@"%d", count];
self.progressView.progress = self.progressView.progress - 0.01;
} completed:^{
// Move along...
}];
This works well enough but, I'm not particularly happy with either the count
variable or with reading the value of self.progressView.progress
in order to decrement it.
I feel like I should be able to spit the signal and bind the properties directly using the RAC
macro. Something like:
RACSignal *baseSignal = [[RACSignal interval:0.05 onScheduler:[RACScheduler mainThreadScheduler]]
take: percentRemaining]
RAC(self, countLabel.text) = [baseSignal
map: ...
...
RAC(self, progressView.progress) = [baseSignal
map: ...
...
The ...
s reveal where I'm stuck. I can't quite get my head around how to compose the RACSignal
such that I don't need to rely on a state variable.
Additionally I'm not sure where/how to inject the // Move along...
side effect I need when the stream completes.
I'm sure both are simple enough once you're thinking the right way but, any help would be really appreciated.
When in doubt, check out
RACSignal+Operations.h
and
RACStream.h,
because there's bound to be an operator for what you want to do. In this case,
the basic missing piece is
-scanWithStart:reduce:.
First of all, though, let's look at the baseSignal
. The logic will stay
basically the same, except that we should publish a
connection
for it:
RACMulticastConnection *timer = [[[RACSignal
interval:0.05 onScheduler:[RACScheduler mainThreadScheduler]]
take:percentRemaining]
publish];
This is so that we can share a single timer between all of the dependent
signals. Although the baseSignal
you provided would also work, that'll
recreate a timer for each subscriber (including dependent signals), which might
lead to tiny variances in their firing.
Now, we can use -scanWithStart:reduce:
to increment the countLabel
and decrement the progressView
. This operator takes previous results and the
current value, and lets us transform or combine them however we want.
In our case, though, we just want to ignore the current value (the NSDate
sent
by +interval:
), so we can just manipulate the previous one:
RAC(self.countLabel, text) = [[[timer.signal
scanWithStart:@0 reduce:^(NSNumber *previous, id _) {
return @(previous.unsignedIntegerValue + 1);
}]
startWith:@0]
map:^(NSNumber *count) {
return count.stringValue;
}];
RAC(self.progressView, progress) = [[[timer.signal
scanWithStart:@(percentRemaining) reduce:^(NSNumber *previous, id _) {
return @(previous.unsignedIntegerValue - 1);
}]
startWith:@(percentRemaining)]
map:^(NSNumber *percent) {
return @(percent.unsignedIntegerValue / 100.0);
}];
The -startWith:
operators in the above may seem redundant, but this is
necessary to ensure that text
and progress
are set before timer.signal
has
sent anything.
Then, we'll just use a normal subscription for completion. It's entirely
possible that these side effects could be turned into signals as well, but it's
hard to know without seeing the code:
[timer.signal subscribeCompleted:^{
// Move along...
}];
Finally, because we used a RACMulticastConnection
above, nothing will actually
fire yet. Connections have to be manually started:
[timer connect];
This connects all of the above subscriptions, and kicks off the timer, so the
values begin flowing to the properties.
Now, this is obviously more code than the imperative equivalent, so one might
ask why it's worthwhile. There are several benefits:
- The value calculations are now thread-safe, because they don't depend on side
effects. If you need to implement something more expensive, it's extremely easy
to move the important work to a background thread.
- Similarly, the value calculations are independent of each other. They
can be easily parallelized if this ever becomes valuable.
- All of the logic is now local to the bindings. You don't have to wonder
where changes are coming from or worry about the ordering (e.g., between
initialization and updating), because it's all in one place and can be read
top-down.
- The values can be calculated without any reference to a view. For
example, in Model-View-ViewModel,
the count and progress would actually be determined in a view
model,
and then the view layer is just a set of dumb bindings.
- The changing values flow from only one input. If you suddenly need to
incorporate another input source (e.g., real progress instead of a timer),
there's only one place you need to change.
Basically, this is a classic example of imperative vs. functional programming.
Although imperative code can start off less complex, it grows in complexity
exponentially. Functional code (and especially functional reactive code) may
start off more complex, but then its complexity grows linearly — it's much
easier to manage as the application grows.