拆分RACSignal以消除状态

Car*_*son 14 cocoa-touch objective-c reactive-programming reactive-cocoa racsignal

我正在使用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...
    }];
Run Code Online (Sandbox Code Playgroud)

这种方法运行得很好但是,我对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: ...
                                        ...
Run Code Online (Sandbox Code Playgroud)

...揭示了我被困的地方.我不能完全理解如何编写RACSignal这样的东西,以至于我不需要依赖状态变量.

另外,我不知道在// Move along...流完成时注入我需要的副作用的位置/方式.

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

Jus*_*ers 38

如有疑问,请查看 RACSignal + Operations.hRACStream.h,因为必须有一个运营商来完成您的工作.在这种情况下,基本的缺失部分是 -scanWithStart:reduce : .

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

RACMulticastConnection *timer = [[[RACSignal
    interval:0.05 onScheduler:[RACScheduler mainThreadScheduler]]
    take:percentRemaining]
    publish];
Run Code Online (Sandbox Code Playgroud)

这样我们就可以在所有相关信号之间共享一个定时器.虽然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);
    }];
Run Code Online (Sandbox Code Playgroud)

-startWith:上述运营商看起来是多余的,但是这是必要的,以确保textprogress被之前设置timer.signal已派出什么.

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

[timer.signal subscribeCompleted:^{
    // Move along...
}];
Run Code Online (Sandbox Code Playgroud)

最后,因为我们使用了RACMulticastConnection上面的内容,实际上什么都不会发生.必须手动启动连接:

[timer connect];
Run Code Online (Sandbox Code Playgroud)

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


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

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

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

尽管命令式代码的起点不那么复杂,但它的复杂性 却呈指数级增长.功能代码(尤其是功能性反应代码)可能起初更复杂,但随后其复杂性呈线性增长- 随着应用程序的增长,管理起来要容易得多.

  • 伙计,你是一个野兽!野兽好!如果我们是一个更大的社区,这篇文章将值得一个恒星答案徽章. (5认同)