使用ReactiveCocoa跟踪远程对象的UI更新

时间:2013-12-13 16:03:04

标签: ios objective-c cocoa-touch uikit reactive-cocoa

我正在制作一款iOS应用,可让您远程控制在桌面上播放的应用中的音乐。

最困难的问题之一是能够正确更新“跟踪器”(显示当前正在播放的歌曲的时间位置和持续时间)的位置。这里有几个输入来源:

  • 启动时,遥控器发送网络请求以获取当前播放歌曲的初始位置和持续时间。
  • 当用户使用遥控器调整跟踪器的位置时,它会向音乐应用程序发送网络请求以更改歌曲的位置。
  • 如果用户使用桌面上的应用程序更改跟踪器的位置,则应用程序会使用跟踪器的新位置向远程控制器发送网络请求。
  • 如果当前正在播放歌曲,则跟踪器的位置每0.5秒左右更新一次。

目前,追踪器是一个UISlider,由“玩家”模型支持。每当用户更改滑块上的位置时,它都会更新模型并发送网络请求,如下所示:

在NowPlayingViewController.m

[[slider rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UISlider *x) {
    [playerModel seekToPosition:x.value];
}];

[RACObserve(playerModel, position) subscribeNext:^(id x) {
    slider.value = player.position;
}];

在PlayerModel.m中:

@property (nonatomic) NSTimeInterval position;

- (void)seekToPosition:(NSTimeInterval)position
{
    self.position = position;
    [self.client newRequestWithMethod:@"seekTo" params:@[positionArg] callback:NULL];
}

- (void)receivedPlayerUpdate:(NSDictionary *)json
{
    self.position = [json objectForKey:@"position"]
}

问题是当用户“摆弄”滑块时,会排队一些网络请求,这些请求都会在不同时间返回。用户可能在收到响应时再次移动滑块,将滑块移回先前的值。

我的问题:如何在此示例中正确使用ReactiveCocoa,确保处理来自网络的更新,但仅限于用户之后没有移动滑块?

1 个答案:

答案 0 :(得分:6)

your GitHub thread about this中,您说要将远程更新视为规范。这很好,因为(正如Josh Abernathy建议的那样),RAC与否,你需要选择两个来源中的一个来获得优先权(或者你需要时间戳,但是你需要一个参考时钟......)。

鉴于您的代码并忽略了RAC,解决方案只是在seekToPosition:中设置一个标志并使用计时器取消设置。检查recievedPlayerUpdate:中的标记,忽略更新(如果已设置)。

顺便说一句,你应该使用RAC()宏来绑定滑块的值,而不是你所拥有的subscribeNext:

RAC(slider, value) = RACObserve(playerModel, position);

但你绝对可以构建一个信号链来做你想做的事情。你需要结合四个信号。

对于最后一项定期更新,您可以使用interval:onScheduler:

[[RACSignal interval:kPositionFetchSeconds
         onScheduler:[RACScheduler scheduler]] map:^(id _){
                          return /* Request position over network */;
}];

map:只是忽略interval:...信号产生的日期,并取出位置。由于来自桌面的请求和消息具有相同的优先级,merge:这些请求和消息一起:

[RACSignal merge:@[desktopPositionSignal, timedRequestSignal]];

但是,如果用户触摸滑块,您决定不希望这些信号通过。这可以通过两种方式之一完成。使用我建议的标志,您可以filter:合并信号:

[mergedSignal filter:^BOOL (id _){ return userFiddlingWithSlider; }];

更好的是 - 避免额外状态 - 将是在throttle:sample:的组合中构建一个操作,该操作在另一个信号具有一定间隔后从信号传递值没发送任何东西:

[mergedSignal sample:
          [sliderSignal throttle:kUserFiddlingWithSliderInterval]];

(当然,您可能希望在合并之前以相同的方式限制/采样interval:onScheduler:信号 - 以避免不必要的网络请求。)

您可以将这些内容放在PlayerModel中,并将其绑定到position。您只需要提供PlayerModel滑块的rac_signalForControlEvents:,然后合并滑块值。由于您在一个链中使用相同的信号多个位置,我相信您想要"multicast"它。

最后,使用startWith:将您的第一个项目(桌面应用中的初始位置)放入流中。

RAC(self, position) = 
    [[RACSignal merge:@[sampledSignal,
                        [sliderSignal map:^id(UISlider * slider){
                                                    return [slider value];
                        }]]
] startWith:/* Request position over network */];

决定将每个信号分解为自己的变量或将它们全部串在一起Lisp风格我会留给你。

顺便说一句,我发现在处理这类问题时实际绘制信号链很有帮助。我made a quick diagram for your scenario。它有助于将信号视为自身的实体,而不是担心它们带来的价值。