Using ReactiveCocoa to Track UI Updates Using a Remote Object

I am creating an iOS application that allows you to remotely control music in an application playing on your desktop.

One of the most difficult problems is to correctly update the position of the "tracker" (which shows the temporary position and duration of the currently playing song). There are several input sources here:

  • At startup, the remote server sends a network request to obtain the starting position and duration of the currently playing song.
  • When the user adjusts the position of the tracker using the remote control, he sends a network request to the music application to change the position of the song.
  • If the user uses the application on the desktop to change the position of the tracker, the application sends a network request to the remote computer with the new position of the tracker.
  • If a song is currently playing, the tracker position is updated every 0.5 seconds or so.

The tracker is currently UISlider, which is supported by the "Player" model. Whenever the user changes position on the slider, he updates the model and sends a network request, for example:

In NowPlayingViewController.m

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

In 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"] } 

The problem is that the user β€œcreaks” using the slider and queues a series of network requests that all return at different times. The user could move the slider again when a response is received by moving the slider back to the previous value.

My question is: how to use ReactiveCocoa correctly in this example, ensuring that updates from the network are resolved, but only if the user has not moved the slider since?

+6
source share
1 answer

On your GitHub thread, you say that you want to treat remote updates as canonical. This is good because (as Josh Abernathy suggested), RAC or not, you need to choose one of two sources for priority (or you need timestamps, but then you need a watch clock ...).

Given your code and not considering RAC, the solution simply sets the flag in seekToPosition: and disables it using a timer. Check the flag in recievedPlayerUpdate: ignoring the update, if installed.

By the way, you should use the RAC() macro to bind your slider value, and not to the subscribeNext: that you have:

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

Of course, you can build a signal chain to do what you want. You have four signals that you need to combine.

For the last item, periodic updates, you can use interval:onScheduler: ::

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

map: simply ignores the date when it generates the interval:... signal, and selects a position. Since your requests and messages from the desktop have the same priority, merge: together:

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

You decided that you did not want any of these signals to pass if the user touched the slider. This can be achieved in one of two ways. Using the suggested flag, you could filter: which combined the signal:

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

Better than this - to avoid an additional state - is to build an operation from a combination of throttle: and sample: which transmits a value from a signal at a certain interval after another signal has sent nothing:

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

(And you, of course, may want to throttle / try the interval:onScheduler: signal in the same way - before merging - to avoid unnecessary network requests.)

You can put all this into PlayerModel by binding it to position . You just need to give the PlayerModel slider rac_signalForControlEvents: and then combine the value of the slider. Since you are using the same signal in several places in the same chain, I believe you want to β€œmulticast” it.

Finally, use startWith: to get your first element above, the actual position from the desktop application, 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 split each signal into its own variable or to combine them all together Lisp-style I will leave you.

By the way, I found it useful to draw signal circuits when working on such problems. I made a quick chart for your scenario . This helps to think of signals as entities in their own right, rather than worrying about the values ​​they carry.

+6
source

All Articles