14

我正在使用 ReactiveCocoa 来更新UILabel倒计时UIProgressView

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...
    }];

这工作得很好,但是,我对count变量或读取值self.progressView.progress以减少它并不特别满意。

我觉得我应该能够吐出信号并直接使用RAC宏绑定属性。就像是:

RACSignal *baseSignal = [[RACSignal interval:0.05 onScheduler:[RACScheduler mainThreadScheduler]]
                            take: percentRemaining]

RAC(self, countLabel.text) = [baseSignal
                                  map: ...
                                  ...

RAC(self, progressView.progress) = [baseSignal
                                        map: ...
                                        ...

...s 揭示了我被困在哪里。我无法完全理解如何编写RACSignal不需要依赖状态变量的内容。

此外,当流完成时,我不确定在哪里/如何注入// Move along...我需要的副作用。

我敢肯定,一旦您以正确的方式思考,两者都足够简单,但是,我们将不胜感激。

4

1 回答 1

38

如有疑问,请查看 RACSignal+Operations.hRACStream.h,因为您想要做的事情肯定有一个运算符。在这种情况下,基本的缺失部分是 -scanWithStart:reduce:

不过,首先让我们看看baseSignal. 逻辑将基本保持不变,除了我们应该为它发布一个连接

RACMulticastConnection *timer = [[[RACSignal
    interval:0.05 onScheduler:[RACScheduler mainThreadScheduler]]
    take:percentRemaining]
    publish];

这样我们就可以在所有相关信号之间共享一个计时器。尽管baseSignal您提供的也可以工作,但这将为每个订阅者(包括相关信号)重新创建一个计时器,这可能会导致它们的触发发生微小差异。

现在,我们可以使用-scanWithStart:reduce:来增加countLabel 减少progressView. 该运算符获取先前的结果和当前值,并让我们根据需要转换或组合它们。

但是,在我们的例子中,我们只想忽略当前值(由NSDate发送+interval:),所以我们可以操纵前一个值:

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);
    }];

上面的-startWith:操作符可能看起来是多余的,但这是必要的,以确保在发送任何内容之前设置textprogress设置。timer.signal

然后,我们将只使用普通订阅来完成。这些副作用也完全有可能转化为信号,但不看代码就很难知道:

[timer.signal subscribeCompleted:^{
    // Move along...
}];

最后,因为我们使用了RACMulticastConnection上面的 a,所以实际上什么都不会触发。必须手动启动连接:

[timer connect];

这将连接所有上述订阅,并启动计时器,因此值开始流向属性。


现在,这显然比命令式等效代码要多,所以有人可能会问为什么它值得。有几个好处:

  1. 值计算现在是线程安全的,因为它们不依赖于副作用。如果你需要实现一些更昂贵的东西,将重要的工作转移到后台线程是非常容易的。
  2. 同样,值计算彼此独立。如果这变得有价值,它们可以很容易地并行化。
  3. 所有的逻辑现在都是本地绑定的。您不必怀疑更改来自何处或担心顺序(例如,在初始化和更新之间),因为它都在一个地方并且可以自上而下阅读。
  4. 可以在不参考任何视图的情况下计算这些值。例如,在Model-View-ViewModel中,计数和进度实际上将在视图模型中确定,然后视图层只是一组哑绑定。
  5. 变化的值仅来自一个输入。如果您突然需要合并另一个输入源(例如,实际进度而不是计时器),那么您只需要更改一个地方。

基本上,这是命令式编程与函数式编程的经典示例。

尽管命令式代码一开始可能不太复杂,但它的复杂性 呈指数增长。函数式代码(尤其是函数式响应式代码)可能一开始会更复杂,但随后其复杂性会线性增长——随着应用程序的增长,它更容易管理。

于 2013-09-17T22:00:23.640 回答