Reactive Cocoa: trouble with switchToLatest in chain of operations - ios

I'm having trouble disposing of one of the operations in a chained list of operations. I'm trying to use Reactive Cocoa to provide a stream of locations while the user is logged in and has granted access to location services.
I'm still pretty new to the world of Reactive Cocoa and functional reactive programming in general, but here's what I've got so far:
#weakify(self);
// signal of BOOL
RACSignal *loggedInSignal = [[self loggedInSignal] distinctUntilChanged];
// signal of CLAuthorizationStatus
RACSignal *currentStateSignal = [[self currentAuthorizationSignal] distinctUntilChanged];
// defer creation of the location stream signal until subscribed to
RACSignal *locationSignal = [RACSignal defer:^RACSignal *
{
#strongify(self);
// To get an uninterrupted stream of locations, just retry the stream if an error occurs
return [[self locationsSignal] retry];
}];
RACSignal *locationServicesDisabledSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
{
#strongify(self);
[self locationServicesDisabled];
[subscriber sendCompleted];
return nil;
}];
self.disposable = [[[[[[loggedInSignal map:^id(NSNumber *loggedIn)
{
return [loggedIn boolValue] ? currentStateSignal : [RACSignal empty];
}]
switchToLatest]
map:^id(NSNumber *currentStatus)
{
if (CLAuthorizationStatusCanBeginMonitoring([currentStatus intValue]))
{
return locationSignal;
}
return locationServicesDisabledSignal;
}]
switchToLatest]
subscribeNext:^(CLLocation *location)
{
#strongify(self);
CLLocationCoordinate2D coord = location.coordinate;
[self.output receivedNewLocationWithLatitude:coord.latitude
longitude:coord.longitude
accuracy:location.horizontalAccuracy
timestamp:location.timestamp];
}]
asScopedDisposable];
This works as expected when the authorisation status changes, starting/stopping the location stream. But when logging out, the location stream continues to provide locations. It's as if authStateSignal is not disposed of, adjusting the authorisation status will still start/stop the stream, even though loggedInSignal had returned NO as its last value.
I tried this:
(previous code as above)...
return locationServicesDisabledSignal;
}]
switchToLatest]
takeUntil:[loggedInSignal skip:1]]
subscribeNext:^(CLLocation *location)
{
(etc)...
after the second switchToLatest to stop that signal after the next log in/out, but besides feeling kind of "hack-ish", it stopped the location stream events after 1 log in/out, to never start them again. What is the proper method of handling this scenario?
Also, it feels like I'm using locationServicesDisabledSignal in the wrong way. I want a method to be called when authStateSignal returns a status indicating the location services are disabled, so that I can react accordingly, but putting the method call inside a [RACSignal createSignal:] block that way doesn't feel very Reactive Cocoa-ey. Any other tips on how to do any of what I'm trying the Reactive Cocoa way would be greatly appreciated too.

The reason it isn't working out for you is that when you return a [RACSignal empty]; values will nor propogate through your chain because [RACSignal empty]; does not send any next events. So Try this:
// signal of BOOL
RACSignal *loggedInSignal = [[self loggedInSignal] distinctUntilChanged];
// signal of CLAuthorizationStatus
RACSignal *currentStateSignal = [[self currentAuthorizationSignal] distinctUntilChanged];
// defer creation of the location stream signal until subscribed to
RACSignal *locationSignal = [RACSignal defer:^RACSignal *
{
#strongify(self);
// To get an uninterrupted stream of locations, just retry the stream if an error occurs
return [[self locationsSignal] retry];
}];
RACSignal *locationServicesDisabledSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
{
#strongify(self);
[self locationServicesDisabled];
[subscriber sendCompleted];
return nil;
}];
self.disposable = [[[RACSignal combineLatest:#[loggedInSignal, currentStateSignal]] flattenMap: ^RACStream * (RACTuple *tuple){
NSNumber* loggedIn = [tuple first];
NSNumber* currentStatus = [tuple second];
if([loggedIn boolValue] && CLAuthorizationStatusCanBeginMonitoring([currentStatus intValue])){
return locationSignal
}
else {
return locationServicesDisabledSignal
}
} ]
subscribeNext:^(CLLocation *location)
{
#strongify(self);
CLLocationCoordinate2D coord = location.coordinate;
[self.output receivedNewLocationWithLatitude:coord.latitude
longitude:coord.longitude
accuracy:location.horizontalAccuracy
timestamp:location.timestamp];
}]

Related

Reactive cocoa issue.How to wait mutiple RACSignal completed then send a next signal

I'm new to reactive cocoa.I want a signal contain three signal, all the three signal completed then sendNext signal.I have tried concat and then operator.And adjust subscriber next or completed location.It always sendNext signal then execute the three signal in it.Sub is my code.Is there any way to fix it?Or use another way.
- (RACSignal *)replacePubRecentContact {
#weakify(self);
return [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) {
NSMutableArray <RIMRecentContactModel *> *contactModelMutableArray = [NSMutableArray new];
//1 Signal first
RACSignal *selectMessageSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[[RIMPublicPostDatabaseManager sharedInstance] selectLastMessage] subscribeNext:^(NSArray <RIMRecentContactModel *> *pubContactModelArray) {
//这里穿回来的lastmMessage是一个pmsgId
for (NSUInteger i = 0; i < pubContactModelArray.count; ++i) {
if (pubContactModelArray[i].uid > 0) {
[contactModelMutableArray addObject:pubContactModelArray[i]];
}
}
[subscriber sendNext:#"pub replace select last message bingo"];
[subscriber sendCompleted];
}];
return nil;
}];
//2 Signal second
RACSignal *selectInfoSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
#strongify(self);
for (NSUInteger j = 0; j < contactModelMutableArray.count; ++j) {
#strongify(self);
[[self selectPubInfoWithPid:contactModelMutableArray[j].uid] subscribeNext:^(RIMPubInfoModel *pubInfoModel) {
contactModelMutableArray[j].username = pubInfoModel.name;
contactModelMutableArray[j].avatar = pubInfoModel.logo;
}];
}
[subscriber sendNext:#"pub replace select info bingo"];
[subscriber sendCompleted];
return nil;
}];
//3 Signal third
RACSignal *replaceSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self.contactDatabaseQueue inDatabase:^(FMDatabase *db) {
for (NSUInteger k = 0; k < contactModelMutableArray.count; ++k) {
RIMRecentContactModel *recentContactModel = contactModelMutableArray[k];
//公众号type 3
recentContactModel.messageType = 3;
NSString *replaceSql = [NSString stringWithFormat:#"REPLACE INTO recentContact (uid, username, avatar, lastMessage, unRead, lastMessageTs, messageType, lastMessageMid, lastMessageSid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"];
//这里因为私聊公众号id重复所以加3
BOOL result = [db executeUpdate:replaceSql, #(recentContactModel.uid), recentContactModel.username, recentContactModel.avatar, recentContactModel.lastMessage, #(recentContactModel.unRead), #(recentContactModel.lastMessageTs), #(recentContactModel.messageType), #(recentContactModel.lastMessageMid), #(recentContactModel.lastMessageSid)];
if (!result) {
NSLog(#"插入最近联系人表失败");
} else {
NSLog(#"插入最近联系人表成功");
}
}
[subscriber sendNext:#"replace公众号最近联系人表成功"];
}];
return nil;
}];
[[[selectMessageSignal concat:selectInfoSignal] then:^RACSignal *{
return replaceSignal;
}] subscribeCompleted:^{
}];
[subscriber sendNext:#"replace pub all bingo"];
[subscriber sendCompleted];
return nil;
}];
}
It seems like you're missing the [subscriber sendCompleted] in your third signal, so that one will never complete. It probably belongs outside of the loop right above return nil.
You can use [RACSignal concat#[selectMessageSignal, selectInfoSignal, replaceSignal] to perform these three signals after each other.
However, your selectInfoSignal does not work as you might expect because it starts another signal for each value in your contactModelMutableArray but does not wait for these signals completion.
Your code sample is actually much more complex than the question suggests. You have a shared dependency between these signals via the contactModelMutableArray Array thats created outside the signals and manipulated by the signals as a side effect, which you should not do because then the "simple" Solution of just concatenating the signals together will not work reliably as mentioned in 2.
It seems to me that what you're actually doing is:
Select all records (plus some filtering: UID has to be > 0)
Loading information for each of these records
Updating all records
I suggest the following structure (just a rough sketch)
-(RACSignal *)replacePubRecentContact {
return [[[[self loadRecords] flattenMap:^RACSignal *(NSArray *records) {
return [[records rac_sequence] signal];
}] flattenMap:^RACSignal *(id record) {
return [self infosForRecord:record];
}] flattenMap:^RACSignal * (id record) {
return [self updateRecord:record];
}];
}
// This function loads all records
-(RACSignal *)loadRecords {
NSLog(#"Return all Records");
return [RACSignal return:#[#1, #2, #3]];
}
// This function load infos for the given record
-(RACSignal *)infosForRecord:(id)record {
NSLog(#"Load Infos for Record %#", record);
return [RACSignal return:[NSString stringWithFormat: #"%# with info", record]];
}
// This function updates the given record
-(RACSignal *)updateRecord:(id)record {
NSLog(#"Update Record: %#", record);
return [RACSignal return:[NSString stringWithFormat: #"Updated - %#", record]];
}
loadRecords sends one value with an array of all records. To then process these records individually, we flattenMap this and send one value for each element of the array (using rac_sequence).
Then its straight forward processing for each record: get infos for that record and then update that records.
The whole replacePubRecentContact signal will complete as soon as all records have been updated.
Edit: I wrote a more detailed explanation in my blog

Reactive Cocoa Legacy - How to handle multiple independent signals in parallel?

I would like to launch multiple independent networking request in parallel, and subscribe to when all requests are done (I don't care if they are completed or error).
For now, I used combineLatest. Thus the requests are made in parallel (as I want), but when a signal sends error, I got an error in the subscription.
I can't find an operator that allows me to launch multiple independent request in parallel, and listen to when all are done.
Sincerely
EDIT
In other cases (where I need to use the signals individually), I need to subscribe to the error. So I still need my signal to be able to sendError:.
My use case is this:
On the one hand, I have an app that can do a full synchronisation. This will launch all independent request. I need to know when all requests are done in order to dismiss the progressHUD. I don't care if my signals have done sendErroror sendCompleted.
But on the other hand, I have multiple buttons, where I can relaunch individually each request, and in this case, I need to tell the user if the signal succeded or failed.
I came up with an answer, using the catch operator:
I add the catch RAC operator to all my signal, that make a new [RACSignal empty]in case the signal fails:
NSArray* allSignals = #[signal1, signal2, signal3, signal4];
NSMutableArray* optionalSignals = [NSMutableArray array];
for (RACSignal* signal in allSignals) {
RACSignal* catchableSignal = [signal catch:^RACSignal *(NSError *error) {
return [RACSignal empty];
}];
[optionalSignals addObject:catchableSignal];
}
return [RACSignal combineLatest:optionalSignals];
This does not change the nature of my individual signals, and I benefit from the combineLatestfeatures.
EDIT
If anyone has a more elegant RAC way to handle the for...in loop, I am still interested.
You can use rac_sequence and map instead of a loop. Also, in your case using catchTo will work in the same way as catch while making code more concise:
NSArray* allSignals = #[signal1, signal2, signal3, signal4];
RACSequence *optionalSignals = [[allSignals rac_sequence] map:^id(RACSignal *signal) {
return [signal catchTo:[RACSignal empty]];
}];
return [RACSignal combineLatest:optionalSignals];
You can try this code:
- (void)getAllItemsWithCompletion:(void (^)())completion {
NSArray *signals = [self.allItems.rac_sequence map:^id(Item *item) {
return [[[self getForItemSignal:item] doCompleted:^{
//done
}] doError:^(NSError *error) {
//Change the button title to retry
}];
}].array;
[[RACSignal combineLatest:signals] subscribeCompleted:^{
NSLog(#"All tasks are done");
}];
}
- (RACSignal *)getForItemSignal:(Item *)item {
return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
[APIClient getFeeForItemUUID:item.UUID
success:^(Fee *fee) {
[subscriber sendNext:#(fee)];
[subscriber sendCompleted];
}
failure:^(NSError *error) {
[subscriber sendError:error];
[subscriber sendCompleted];
}];
}];
}

How to prevent a signal to send a next event before subsequent asynchronous calls are completed?

I am willing to prevent a RAC signal to send a next event message before some subsequents calls are completed.
Here is an example of how i have proceed so far:
- (RACSignal *) fetchNearbyDatasForLocation: (CLLocationCoordinate2D)coordinate {
RACSignal* finalSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
[subscriber sendNext:self.rawDatas];
[subscriber sendCompleted];
return nil;
}];
return [[[self.apiClient fetchNearbyDatasforLocation:coordinate]
flattenMap:^RACStream *(NSArray* datas) {
NSMutableArray* signals = [[NSMutableArray alloc] init];
self.rawDatas = datas;
for (SNPDataModel* data in datas) {
RACSignal* fetchExtraDataSignal = [self.apiClient fetchExtraDataInfoForData:data];
[signals addObject:fetchExtraDataSignal];
RAC(data, extraData) = fetchExtraDataSignal;
}
// will send a next message when all asynchronous call are completed
RACSignal* completedSignal = [RACSignal combineLatest:signals];
return completedSignal;
}]
flattenMap:^RACStream *(RACTuple* value) {
return finalSignal;
}];
}
Few explanation about my code here: I am trying to fetch an array of datas and send a signal with the same array when all fetchExtraDataSignal signals have completed (so the array should now have the extra data content at that time). Is there a better reactive way to achieve this? I would like to not rely on the property datas if that's possible.
Although I don't have self-confidence in understanding what you are meaning,
I recommend using "map", instead of the combination of "createSignal" and "flattenMap".
Like this.
- (RACSignal *) fetchNearbyDatasForLocation: (CLLocationCoordinate2D)coordinate {
return [[self.apiClient fetchNearbyDatasforLocation:coordinate]
flattenMap:^RACStream *(NSArray* datas)
{
/* ... */;
return [[RACSignal combineLatest:signals]
map:^(RACTuple *unused) { return datas; }]];
}];
}
And I strongly recommend don't use any mutable fields to communicate data among event handlers. That causes results of these become unpredictable when multiple signals are simultaneously working.

Error block not called while using ReactiveCocoa

For some reason I am not getting the error message to come through. (I've simplified the code here to get straight to the point.)
// Send an error message
_loginButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendError:error]; // Pretend this is a real error
return nil;
}];
}];
// Subscribe to loginButton's returned signal
[_loginButton.rac_command.executionSignals subscribeNext:^(RACSignal *loginSignal) {
[loginSignal subscribeError:^(NSError *error) {
NSLog(#"A");
} completed:^{
NSLog(#"B");
}];
}];
This prints "B". Any idea why? If -sendError: is called on the subscriber, why does the completion block receive it?
As you've discovered, RACCommand automatically catches errors within executionSignals.
This is intended to be a convenience for operators like -flatten, -concat, and -switchToLatest, which would otherwise prematurely terminate if an error occurs on any of the inner signals.
If all you care about is knowing when an error occurs, you should use RACCommand.errors instead. If you want to know where the error originated, checking the error domain and code may be easier (or at least more intuitive) than subscribing to the error event of each inner signal.
Subscriptions-within-subscriptions, and even subscriptions in general, are something of a code smell in RAC. Even if you don't want to use errors, there are generally higher-level operators to accomplish what you want (like using -map: to apply a -catch: to each inner signal).
This seems to work (materialize it and dematerialize it) based on this suggestion.
// Send an error message
_loginButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendError:error]; // Pretend this is a real error
return nil;
}] materialize];
}];
// Subscribe to loginButton's returned signal
[_loginButton.rac_command.executionSignals subscribeNext:^(RACSignal *loginSignal) {
[[loginSignal dematerialize] subscribeError:^(NSError *error) {
NSLog(#"A");
} completed:^{
NSLog(#"B");
}];
}];

How to conditionally buffer RACSignal values?

I'm working on some code that interacts with a remote API via websockets. My data layer is responsible for establishing and monitoring the websocket connection. It also contains methods that can be used by the application to enqueue websocket messages to be sent. The application code should not be responsible for inspecting the state of the websocket connection, aka fire-and-forget.
Ideally, I'd like to data layer to function as follows:
When the data layer does not have a connection to the websocket endpoint (self.isConnected == NO), messages are buffered internally.
When a connection is becomes available (self.isConnected == YES), buffered messages are immediately sent, and any subsequent messages are sent immediately.
Here's what I've been able to come up with:
#import "RACSignal+Buffering.h"
#implementation RACSignal (Buffering)
- (RACSignal*)bufferWithSignal:(RACSignal*)shouldBuffer
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];
NSMutableArray* bufferedValues = [[NSMutableArray alloc] init];
__block BOOL buffering = NO;
void (^bufferHandler)() = ^{
if (!buffering)
{
for (id val in bufferedValues)
{
[subscriber sendNext:val];
}
[bufferedValues removeAllObjects];
}
};
RACDisposable* bufferDisposable = [shouldBuffer subscribeNext:^(NSNumber* shouldBuffer) {
buffering = shouldBuffer.boolValue;
bufferHandler();
}];
if (bufferDisposable)
{
[disposable addDisposable:bufferDisposable];
}
RACDisposable* valueDisposable = [self subscribeNext:^(id x) {
[bufferedValues addObject:x];
bufferHandler();
} error:^(NSError *error) {
[subscriber sendError:error];
} completed:^{
[subscriber sendCompleted];
}];
if (valueDisposable)
{
[disposable addDisposable:valueDisposable];
}
return disposable;
}];
}
#end
Lastly, this is pseudo-code for how it would be used:
#interface APIManager ()
#property (nonatomic) RACSubject* requests;
#end
#implementation WebsocketDataLayer
- (id)init
{
self = [super init];
if (self) {
RACSignal* connectedSignal = RACObserve(self, connected);
self.requests = [[RACSubject alloc] init];
RACSignal* bufferedApiRequests = [self.requests bufferWithSignal:connectedSignal];
[self rac_liftSelector:#selector(sendRequest:) withSignalsFromArray:#[bufferedApiRequests]];
}
return self;
}
- (void)enqueueRequest:(NSString*)request
{
[self.requests sendNext:request];
}
- (void)sendRequest:(NSString*)request
{
DebugLog(#"Making websocket request: %#", request);
}
#end
My question is: Is this the right approach for buffering values? Is there a more idiomatic RAC way of handling this?
Buffering can be thought of as something that applies to individual requests, which leads to a natural implementation using -flattenMap: and RACObserve:
#weakify(self);
RACSignal *bufferedRequests = [self.requests flattenMap:^(NSString *request) {
#strongify(self);
// Waits for self.connected to be YES, or checks that it already is,
// then forwards the request.
return [[[[RACObserve(self, connected)
ignore:#NO]
take:1]
// Replace the property value with our request.
mapReplace:request];
}];
If ordering is important, you can replace -flattenMap: with -map: plus -concat. These implementations avoid the need for any custom operators, and work without manual subscriptions (which are notoriously messy).
You do almost exactly the same as what is implemented in the bufferWithTime: operation and I can't think of any existing operations that would implement it more idiomatically. (Probably this is the reason why bufferWithTime was implemented in this way.) Reviewing your code using that implementation may reveal some faults you didn't think of.
But to be honest, this should not be so hard. There should exist a buffering operation that buffers the output and spews the contents when the trigger signal fires. Probably most buffering can be implemented in terms of this functionality, so having it would add value to the framework.

Resources