ReactiveCocoa: how to keep signal until the server is connected? - ios

I have two signals (RACSignal):
synchronizeToServerSignal: a signal containing data to be sent to server.
serverConnectionSignal: a signal containing a boolean value to indicate the connection state.
The synchronizeToServerSignal will be triggered when a content is changed, and this signal is controlled by serverConnectionSignal:
If the latest value of serverConnectionSignal is YES, synchronizeToServerSignal will keep sending the next value (data).
If the latest value of serverConnectionSignal is NO, synchronizeToServerSignal will keep holding the latest value and discarding the old values. Once serverConnectionSignal sends YES, serverConnectionSignal will keep sending the next value again.
How to use ReactiveCocoa to achieve this?

replayLast will give you a signal that holds onto the latest value sent and discards old values. Then if:then:else: allows you to switch between two signals based on the value of a third boolean signal.
But what to switch between? Since you just want to ignore values, you can just use [RACSignal empty] -- thus switching between a signal with a useful value and a signal with no values:
[RACSignal if:serverConnectionSignal
then:[synchronizeToServerSignal replayLast]
else:[RACSignal empty]] subscribeNext:^(id x) {
// sync here
}];
The intent of this code would be made a lot more clear by a helper method in a category, though:
#implementation RACSignal (Helpers)
- (RACSignal *)guard:(RACSignal *)boolSignal {
return [RACSignal if:boolSignal then:self else:[RACSignal empty]];
}
#end
Then you can just write:
[[synchronizeToServerSignal replayLast] guard:serverConnectionSignal]
Much more clear!

Related

Reading Bluetooth LE CBCharacteristic returns a smaller value on iOS 7 [duplicate]

I have a characteristic value which contains the data for an image. In the peripheral I setup the value like this:
_photoUUID = [CBUUID UUIDWithString:bPhotoCharacteristicUUID];
_photoCharacteristic = [[CBMutableCharacteristic alloc] initWithType:_photoUUID
properties:CBCharacteristicPropertyRead
value:Nil
permissions:CBAttributePermissionsReadable];
My understanding is that when this value is requested, the didReceiveReadRequest callback will be called:
-(void) peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqual:_photoUUID]) {
if (request.offset > request.characteristic.value.length) {
[_peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
return;
}
else {
// Get the photos
if (request.offset == 0) {
_photoData = [NSKeyedArchiver archivedDataWithRootObject:_myProfile.photosImmutable];
}
request.value = [_photoData subdataWithRange:NSMakeRange(request.offset, request.characteristic.value.length - request.offset)];
[_peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
}
}
}
This comes pretty much from Apple's documentation. On the Central side in the didDiscoverCharacteristic callback I have the following code:
if ([characteristic.UUID isEqual:_photoUUID]) {
_photoCharacteristic = characteristic;
[peripheral readValueForCharacteristic:characteristic];
}
Which in turn calls the didUpdateValueForCharacteristic callback:
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
NSLog(#"updated value for characteristic");
if ([characteristic.UUID isEqual:_photoUUID]) {
NSArray * photos = [NSKeyedUnarchiver unarchiveObjectWithData:characteristic.value];
}
}
All of the callbacks are called but when I try to re-construct the array, it's corrupted because not all of the data is transferred correctly. I would expect the didRecieveReadRequest callback to be called multiple times with a different offset each time. However it's only called once.
I was wondering if anyone knew what I'm doing wrong?
I'm guessing you're bumping up against the 512 byte limit on characteristic length. You'll need to move to subscriptions to characteristics and processing of updates to get around this:
On the central:
Subscribe to the characteristic by calling -[CBPeripheral setNotifyValue:forCharacteristic] (with YES as the notify value).
In -peripheral:didUpdateValueForCharacteristic:error, every update will either be data to append, or something you choose to use on the peripheral side to indicate end-of-data (I use an empty NSData for this). Update your -peripheral:didUpdateValueForCharacteristic:error code so that:
If you're starting to read a value, initialize a sink for the incoming bytes (e.g. an NSMutableData).
If you're in the middle of reading a value, you append to the sink.
If you see the EOD marker, you consider the transfer complete. You may wish to unsubscribe from the characteristic at this state, by calling -[CBPeripheral setNotifyValue:forCharacteristic] with a notify value of NO.
-peripheral:didUpdateNotificationStateForCharacteristic:error: is a good spot to manage the initialization and later use of the sink into which you read chunks. If characteristic.isNotifying is updated to YES, you have a new subscription; if it's updated to NO then you're done reading. At this point, you can use NSKeyedUnarchiver to unarchive the data.
On the peripheral:
In -[CBMutableCharacteristic initWithType:properties:value:permissions], make sure the properties value includes CBCharacteristicPropertyNotify.
Use -peripheralManager:central:didSubscribeToCharacteristic: to kick off the chunking send of your data, rather than -peripheral:didReceiveReadRequest:result:.
When chunking your data, make sure your chunk size is no larger than central.maximumUpdateValueLength. On iOS7, between an iPad 3 and iPhone 5, I've typically seen 132 bytes. If you're sending to multiple centrals, use the least common value.
You'll want to check the return code of -updateValue:forCharacteristic:onSubscribedCentrals; if underlying queue backs up, this will return NO, and you'll have to wait for a callback on -peripheralManagerIsReadyToUpdateSubscribers: before continuing (this is one of the burrs in an otherwise smooth API, I think). Depending upon how you handle this, you could paint yourself into a corner because:
If you're constructing and sending your chunks on the same queue that the peripheral is using for its operations, AND doing the right thing and checking the return value from -updateValue:forCharacteristic:onSubscribedCentrals:, it's easy to back yourself into a non-obvious deadlock. You'll either want to make sure that you yield the queue after each call to -updateValue:forCharacteristic:onSubscribedCentrals:, perform your chunking loop on a different queue than the peripheral's queue (-updateValue:forCharacteristic:onSubscribedCentrals: will make sure its work is done in the right place). Or you could get fancier; just be mindful of this.
To see this in action, the WWDC 2012 Advanced Core Bluetooth video contains an example (sharing VCards) that covers most of this. It doesn't however, check the return value on the update, so they avoid the pitfalls in #4 altogether.
Hope that helps.
I tried the approach described by Cora Middleton, but couldn't get it to work. If I understand her approach correctly, she would send all partial data through the update notifications. The problem for me seemed to be that there was no guarantee each update would be read by the central if the values in these notifications would change often in short succession.
So because that approach didn't work, I did the following:
There's some characteristic that I use to keep track of the state of the peripheral. This characteristic would only contain some flags and would send out notifications if one or more flags change. Interactions by the user on the peripheral would change the state and there's one action on the peripheral that the user can perform to trigger a download from a connected central.
The data to be downloaded from the central is added to a stack on the peripheral. The last item on the stack is a terminator indicator (an empty NSData object)
The central registers to receive notifications of the aforementioned state characteristic. If some flag is set, a download is triggered.
On the peripheral side, every time I receive a read request for a certain characteristic, I remove 1 item from the stack and return this item.
On the central side I add all data that is returned from the read requests. If the empty data value is retrieved, then I create an object from the returned data (in my case it's a JSON string).
On the peripheral side I also know the download is finished after returning the empty NSData object, so afterwards I can change the state once again for the peripheral.

How to know when all objects are saved asynchronously using ReactiveCocoa

In my app, I am using ReactiveCocoa to return signals to notify me when async api calls are completed (successfully or not). On the POST for saving the data, it only takes one object at a time:
- (RACSignal *)postJSONData:(NSDictionary *)dict toRelativeURL:(NSString *)urlString;.
The function that returns a RACSignal sends the subscriber a next:
[subscriber sendNext:json]or an Error: [subscriber sendError:jsonError].
This works great when saving a single object but I also have a scenario where I have to save multiple objects. These objects can be saved in any order (i.e. they are not dependent on one another) or sequentially - it doesn't matter to me.
I need to update the UI indicating the overall progress (Saving 1 of 4, Saving 2 of 4....) as well as a final progress update (Completed 4 of 4) and specific action to take when all have been processed (successful or not).
There are a number of ways to do this, but I'd like to do this the proper way using ReactiveCocoa. I'm thinking I either can do this with a concat: or then: with a rac_sequence map:^, but I'm not sure. On their github page, they show an example of addressing parallel work streams, but they use 2 discretely defined signals. I won't have my signals until I loop through each object I need to save. Would love some guidance or an example (even better!). Thanks in advance.
I'm doing something similar in my app where I start 3 different async network calls and combine them all into one signal that I can listen to. Basically I loop through all my objects and store the network signal in an array. I then call merge: and pass it the array of network signals.
NSMutableArray *recievedNames = [NSMutableArray new];
NSMutableArray *signals = [NSMutableArray new];
//go though each database that has been added and grab a signal for the network request
for (GLBarcodeDatabase *database in self.databases) {
[signals addObject:[[[[self.manager rac_GET:[database getURLForDatabaseWithBarcode:barcode] parameters:nil] map:^id(RACTuple *value) {
return [((NSDictionary *)value.second) valueForKeyPath:database.path];
}] doError:^(NSError *error) {
NSLog(#"Error while fetching name from database %#", error);
}]
}
//forward all network signals into one signal
return [[[RACSignal merge:signals] doNext:^(NSString *x) {
[recievedNames addObject:x];
}] then:^RACSignal *{
return [RACSignal return:[self optimalNameForBarcodeProductWithNameCollection:recievedNames]];
}];
Feel free to ask me questions about any of the operators I have used and I will do my best to explain them.
I am just learning ReactiveCocoa myself but I wanted to point out something important which also agrees with lightice11's answer. concat combines signals sequentially. Meaning you won't get anything from #2 or #3 until #1 completes. merge on the other hand interleaves the responses returning whatever comes next regardless of order. So for your scenario, it sounds like you really do want merge.
To quote the man, Justin Spahr-Summers:
concat joins the streams sequentially, merge joins the signals on an as-soon-as-values-arrive basis, switch only passes through the events from the latest signal.

How to make RACSignal to become hot?

ReactiveCocoa can convert the signal to "hot" signal by calling its -subscribeCompleted:. But I think this method is quite verbose if you do not care about the result (i.e. no subscribers).
RACDisposable *animationDisposable = [[self play:animation] subscribeCompleted:^{
// just to make the animation play
}];
And these 3 lines are not expressive enough to show my intention.
Is there any method for similar purpose? Thanks!
I want to do nothing except making it hot (=make it run once).
"You keep using that word. I do not think it means what you think it means."
A "hot signal" is a signal that sends values (and presumably does work) regardless of whether it has any subscribers. A "cold signal" is a signal that defers its work and the sending of any values until it has a subscriber. And a cold signal will perform its work and send values for each subscriber.
If you want to make a cold signal run only once but have multiple subscribers, you need to multicast the signal. Multicasting is a pretty simple concept, that works like this:
Create a RACSubject to proxy the values sent by the signal you want to execute once.
Subscribe to the subject as many times as needed.
Create a single subscription to the signal you want to execute only once, and for every value sent by the signal, send it to the subject with [subject sendNext:value].
However, you can and should use RACMulticastConnection to do all of the above with less code:
RACMulticastConnection *connection = [signal publish];
[connection.signal subscribe:subscriberA];
[connection.signal subscribe:subscriberB];
[connection.signal subscribe:subscriberC];
[connection connect]; // This will cause the original signal to execute once.
// But each of subscriberA, subscriberB, and subscriberC
// will be sent the values from `signal`.
If you do not care about the output of the signal (and for some reason you really want play to be a signal), you may want to make a command. A command causes a signal to be executed via some sort of event (such as a ui button press or other event). Simply create the Signal, add it to a command, then when you need to run it, execute it.
#weakify(self);
RACCommand * command = [[RACCommand alloc] initWithSignalBlock:^(id input) {
#strongify(self);
return [self play:animation];
}];
//This causes the signal to be ran
[command execute:nil];
//Or you could assign the command to a button so it is executed
// when the button is pressed
playButton.rac_command = command;

Using ReactiveCocoa to track UI updates with a remote object

I'm making an iOS app which lets you remotely control music in an app playing on your desktop.
One of the hardest problems is being able to update the position of the "tracker" (which shows the time position and duration of the currently playing song) correctly. There are several sources of input here:
At launch, the remote sends a network request to get the initial position and duration of the currently playing song.
When the user adjusts the position of the tracker using the remote, it sends a network request to the music app to change the position of the song.
If the user uses the app on the desktop to change the position of the tracker, the app sends a network request to the remote with the new position of the tracker.
If the song is currently playing, the position of the tracker is updated every 0.5 seconds or so.
At the moment, the tracker is a UISlider which is backed by a "Player" model. Whenever the user changes the position on the slider, it updates the model and sends a network request, like so:
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 when a user "fiddles" with the slider, and queues up a number of network requests which all come back at different times. The user could be have moved the slider again when a response is received, moving the slider back to a previous value.
My question: How do I use ReactiveCocoa correctly in this example, ensuring that updates from the network are dealt with, but only if the user hasn't moved the slider since?
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.
For the last item, the periodic update, you can use interval:onScheduler::
[[RACSignal interval:kPositionFetchSeconds
onScheduler:[RACScheduler scheduler]] map:^(id _){
return /* Request position over network */;
}];
The map: just ignores the date that the interval:... signal produces, and fetches the position. Since your requests and messages from the desktop have equal priority, merge: those together:
[RACSignal merge:#[desktopPositionSignal, timedRequestSignal]];
You decided that you don't want either of those signals going through if the user has touched the slider, though. This can be accomplished in one of two ways. Using the flag I suggested, you could filter: that merged signal:
[mergedSignal filter:^BOOL (id _){ return userFiddlingWithSlider; }];
Better than that -- avoiding extra state -- would be to build an operation out of a combination of throttle: and sample: that passes a value from a signal at a certain interval after another signal has not sent anything:
[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.

Reading long characteristic values using CoreBluetooth

I have a characteristic value which contains the data for an image. In the peripheral I setup the value like this:
_photoUUID = [CBUUID UUIDWithString:bPhotoCharacteristicUUID];
_photoCharacteristic = [[CBMutableCharacteristic alloc] initWithType:_photoUUID
properties:CBCharacteristicPropertyRead
value:Nil
permissions:CBAttributePermissionsReadable];
My understanding is that when this value is requested, the didReceiveReadRequest callback will be called:
-(void) peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqual:_photoUUID]) {
if (request.offset > request.characteristic.value.length) {
[_peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
return;
}
else {
// Get the photos
if (request.offset == 0) {
_photoData = [NSKeyedArchiver archivedDataWithRootObject:_myProfile.photosImmutable];
}
request.value = [_photoData subdataWithRange:NSMakeRange(request.offset, request.characteristic.value.length - request.offset)];
[_peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
}
}
}
This comes pretty much from Apple's documentation. On the Central side in the didDiscoverCharacteristic callback I have the following code:
if ([characteristic.UUID isEqual:_photoUUID]) {
_photoCharacteristic = characteristic;
[peripheral readValueForCharacteristic:characteristic];
}
Which in turn calls the didUpdateValueForCharacteristic callback:
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
NSLog(#"updated value for characteristic");
if ([characteristic.UUID isEqual:_photoUUID]) {
NSArray * photos = [NSKeyedUnarchiver unarchiveObjectWithData:characteristic.value];
}
}
All of the callbacks are called but when I try to re-construct the array, it's corrupted because not all of the data is transferred correctly. I would expect the didRecieveReadRequest callback to be called multiple times with a different offset each time. However it's only called once.
I was wondering if anyone knew what I'm doing wrong?
I'm guessing you're bumping up against the 512 byte limit on characteristic length. You'll need to move to subscriptions to characteristics and processing of updates to get around this:
On the central:
Subscribe to the characteristic by calling -[CBPeripheral setNotifyValue:forCharacteristic] (with YES as the notify value).
In -peripheral:didUpdateValueForCharacteristic:error, every update will either be data to append, or something you choose to use on the peripheral side to indicate end-of-data (I use an empty NSData for this). Update your -peripheral:didUpdateValueForCharacteristic:error code so that:
If you're starting to read a value, initialize a sink for the incoming bytes (e.g. an NSMutableData).
If you're in the middle of reading a value, you append to the sink.
If you see the EOD marker, you consider the transfer complete. You may wish to unsubscribe from the characteristic at this state, by calling -[CBPeripheral setNotifyValue:forCharacteristic] with a notify value of NO.
-peripheral:didUpdateNotificationStateForCharacteristic:error: is a good spot to manage the initialization and later use of the sink into which you read chunks. If characteristic.isNotifying is updated to YES, you have a new subscription; if it's updated to NO then you're done reading. At this point, you can use NSKeyedUnarchiver to unarchive the data.
On the peripheral:
In -[CBMutableCharacteristic initWithType:properties:value:permissions], make sure the properties value includes CBCharacteristicPropertyNotify.
Use -peripheralManager:central:didSubscribeToCharacteristic: to kick off the chunking send of your data, rather than -peripheral:didReceiveReadRequest:result:.
When chunking your data, make sure your chunk size is no larger than central.maximumUpdateValueLength. On iOS7, between an iPad 3 and iPhone 5, I've typically seen 132 bytes. If you're sending to multiple centrals, use the least common value.
You'll want to check the return code of -updateValue:forCharacteristic:onSubscribedCentrals; if underlying queue backs up, this will return NO, and you'll have to wait for a callback on -peripheralManagerIsReadyToUpdateSubscribers: before continuing (this is one of the burrs in an otherwise smooth API, I think). Depending upon how you handle this, you could paint yourself into a corner because:
If you're constructing and sending your chunks on the same queue that the peripheral is using for its operations, AND doing the right thing and checking the return value from -updateValue:forCharacteristic:onSubscribedCentrals:, it's easy to back yourself into a non-obvious deadlock. You'll either want to make sure that you yield the queue after each call to -updateValue:forCharacteristic:onSubscribedCentrals:, perform your chunking loop on a different queue than the peripheral's queue (-updateValue:forCharacteristic:onSubscribedCentrals: will make sure its work is done in the right place). Or you could get fancier; just be mindful of this.
To see this in action, the WWDC 2012 Advanced Core Bluetooth video contains an example (sharing VCards) that covers most of this. It doesn't however, check the return value on the update, so they avoid the pitfalls in #4 altogether.
Hope that helps.
I tried the approach described by Cora Middleton, but couldn't get it to work. If I understand her approach correctly, she would send all partial data through the update notifications. The problem for me seemed to be that there was no guarantee each update would be read by the central if the values in these notifications would change often in short succession.
So because that approach didn't work, I did the following:
There's some characteristic that I use to keep track of the state of the peripheral. This characteristic would only contain some flags and would send out notifications if one or more flags change. Interactions by the user on the peripheral would change the state and there's one action on the peripheral that the user can perform to trigger a download from a connected central.
The data to be downloaded from the central is added to a stack on the peripheral. The last item on the stack is a terminator indicator (an empty NSData object)
The central registers to receive notifications of the aforementioned state characteristic. If some flag is set, a download is triggered.
On the peripheral side, every time I receive a read request for a certain characteristic, I remove 1 item from the stack and return this item.
On the central side I add all data that is returned from the read requests. If the empty data value is retrieved, then I create an object from the returned data (in my case it's a JSON string).
On the peripheral side I also know the download is finished after returning the empty NSData object, so afterwards I can change the state once again for the peripheral.

Resources