6

我正在制作一个 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,确保处理来自网络的更新,但前提是用户没有移动滑块?

4

1 回答 1

6

In your GitHub thread about this you say that you want to consider the remote's updates as canonical. That's good, because (as Josh Abernathy suggested there), RAC or not, you need to pick one of the two sources to take priority (or you need timestamps, but then you need a reference clock...).

Given your code and disregarding RAC, the solution is just setting a flag in seekToPosition: and unsetting it using a timer. Check the flag in recievedPlayerUpdate:, ignoring the update if it's set.

By the way, you should use the RAC() macro to bind your slider's value, rather than the subscribeNext: that you've got:

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

You can definitely construct a signal chain to do what you want, though. You've got four signals you need to combine.

对于最后一项,定期更新,您可以使用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]];

(And you might, of course, want to throttle/sample the interval:onScheduler: signal in the same way -- before the merge -- in order to avoid unncessary network requests.)

You can put this all together in PlayerModel, binding it to position. You'll just need to give the PlayerModel the slider's rac_signalForControlEvents:, and then merge in the slider value. Since you're using the same signal multiple places in one chain, I believe that you want to "multicast" it.

Finally, use startWith: to get your first item above, the inital position from the desktop app, into the stream.

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

The decision to break each signal out into its own variable or string them all together Lisp-style I'll leave to you.

Incidentally, I've found it helpful to actually draw out the signal chains when working on problems like this. I made a quick diagram for your scenario. It helps with thinking of the signals as entities in their own right, as opposed to worrying about the values that they carry.

于 2014-01-03T08:44:49.457 回答