拆分RACSignal以消除状态

时间:2013-09-17 14:38:00

标签: objective-c cocoa-touch reactive-programming reactive-cocoa racsignal

我正在使用ReactiveCocoa更新UILabelUIProgressView倒计时:

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: ...
                                        ...

...显示我被卡住的地方。我无法理解如何编写RACSignal这样我不需要依赖状态变量。

此外,我不确定在流完成时我需要注入// Move along...副作用的位置/方式。

一旦你想到了正确的方法,我确信两者都很简单,但是,任何帮助都会非常感激。

1 个答案:

答案 0 :(得分:38)

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

首先,让我们看一下baseSignal。逻辑将保持不变 基本相同,只是我们应该发布a connection 为它:

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:运算符可能看起来多余,但确实如此 确保在text之前设置progresstimer.signal所必需的 送了什么。

然后,我们将使用正常订阅完成。这完全是 可能这些副作用也可以变成信号,但它是 很难知道没有看到代码:

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

最后,因为我们上面使用了RACMulticastConnection,所以实际上什么都没有 火了。必须手动启动连接:

[timer connect];

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


现在,这显然是比命令式等价物更多的代码,所以有人可能 问为什么这是值得的。有几个好处:

  1. 值计算现在是线程安全,因为它们不依赖于边 效果。如果你需要实现更昂贵的东西,这非常容易 将重要的工作转移到后台线程。
  2. 同样,价值计算彼此独立。他们 如果它变得有价值,可以很容易地并行化。
  3. 所有逻辑现在绑定本地。你不必怀疑 变化来自或担心排序(例如,之间) 初始化和更新),因为它都在一个地方,可以读取 自上而下。
  4. 可以计算值,而不引用任何视图。对于 例如,在Model-View-ViewModel中, 计数和进度实际上将在view model中确定, 然后视图层只是一组哑的绑定。
  5. 更改的值来自仅一个输入。如果你突然需要 合并另一个输入源(例如,真正的进度而不是计时器), 只有一个地方你需要改变。
  6. 基本上,这是命令式与函数式编程的典型例子。

    虽然命令性代码可以从较不复杂的开始,但它的复杂性会增加 呈指数。功能代码(尤其是功能性反应代码)可以 从更复杂的开始,然后它的复杂性增长线性 - 它是很多 随着应用程序的增长,更容易管理。