Splitting an RACSignal to eliminate state

前端 未结 1 1208
抹茶落季
抹茶落季 2021-01-30 02:46

I\'m using ReactiveCocoa to update a UILabel whilst a UIProgressView counts down:

NSInteger percentRemaining = ...;
self.progressView.p         


        
相关标签:
1条回答
  • 2021-01-30 03:18

    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:

    1. 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.
    2. Similarly, the value calculations are independent of each other. They can be easily parallelized if this ever becomes valuable.
    3. 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.
    4. 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.
    5. 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.

    0 讨论(0)
提交回复
热议问题