How to Accomplish this w/ ReactiveCocoa - ios

I'm building a feature where users of my app can find their Facebook friends and add them in the app. There are three steps which I have to do:
Get the currently connected users
Get the Facebook users
Get the Application Users (this is dependent on step 2)
Once all of these complete I then need to combine/reduce the three resulting arrays into a final array.
I have created three functions that all return RACSignal
getUsersWithFacebookIds, getConnectedUsers, and getFacebookUsers
I'm not sure how to wire all of this using ReactiveCocoa.

The Once All Are Done Do Something With All, can be achieve with:
[[RACSignal combineLatest:#[connectUsersSignal,facebookUsersSignal,applicationUsersSignal]] subscribeNext:^(RACTuple *users) {
NSArray *connectedUsers = [users first];
NSArray *facebookUsers = [users second];
NSArray *applicationUsers = [users third];
}];
The other piece missing is how you make the applicationUsersSignal dependent on the facbookUsersSignal. Which could be done like this:
- (RACSignal *)applicationUsersSignalWithFacebookUsersSignal:(RACSignal *)fbSignal
{
return [fbSignal flattenMap:^RACStream *(NSArray *facebookUsers) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// Do what you have to do with the facebookUsers
return nil;
}];
}];
}
Just to add a bit more to the answer. I am assuming those are cold signals, (signals that haven't started yet aka haven't been subscribed). So the idea of using combineLatest: is that you want to capture the point where each and every single signal as send at least one next, after that you subscribe to it so it can begin. Finally you can get their values from a RACTuple.
I re read your question and you want them to come all in a single Array:
[[[RACSignal combineLatest:#[connectUsersSignal,facebookUsersSignal,applicationUsersSignal]] map:^id(RACTuple *allArrays) {
return [allArrays.rac_sequence foldLeftWithStart:[NSMutableArray array] reduce:^id(id accumulator, id value) {
[accumulator addObjectsFromArray:value];
return accumulator;
}];
}] subscribeNext:^(NSArray *allUsers) {
// Do Something
}];

Related

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];
}];
}];
}

ReactiveCoca Design Pattern for Forwarding / Chaining Signals

I have the following classes and methods:
Class A
- (RACSignal *)createX
{
NSDictionary *parameters = #{};
return [[[[HTTPClient sharedClient] rac_POST:#"X/" parameters:parameters]
map:^id(OVCResponse *response) {
[self logResponse:response];
return response.result;
}] catch:^RACSignal *(NSError *error) {
return [RACSignal error:[self handleError:error]];
}];
}
Class B
- (void)requestData
{
[[self.myClassA createX]
subscribeNext:^(NSArray *results) {
DDLogDebug(#"response : %#", results);
}
error:^(NSError *error) {
[self.dataManager sendError:error];
}];
}
Class C
- (void)retrieveData
{
[self.myClassB requestData];
}
What is the best way to design requestData in Class B such that the results array can be accessed in Class C.
i.e.
Should I forward the Array some way using [array rac_sequence],
should I create a new signal inside requestData, should requestData return a RACSignal instead of void?
Any help or guidance would be greatly appreciated. Thanks.
I believe you want to use doNext instead of subscribeNext in Class B.
I'm not entirely clear on your use case here but I think you are mixing paradigms. RAC stuff is always asynchronous so in order to access the result of your network request synchronously you will have to store it in some way.
You could bind the result to a property on ClassB or you could use RACCommand, something like:
[[RACCommand alloc] initWithEnabled:RACObserve(self, valid) signalBlock:^RACSignal *(id input) {
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//make network call
//send responseObject to subscriber
[subscriber sendNext:responseObject];
//[subscriber sendError:#NSError#] //send error if something went wrong
[subscriber sendCompleted];
return nil;
}] materialize];
}];
You can then subscribe to the executionSignals of the RACCommand which streams a RACSignal for each execution of the command which you have control over in the block described above.
So I think your options are:
RACCommand pattern, look into it a bit more
Bind result of network call to a property for synchronous access
Return a RACSignal as you describe upon calling the method
Possibly look into replay() or replayLast() here as you could then store a reference to the RACSignal and subscribe to it for access to its last value

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.

The best practice to handle chaining requests using reactive cocoa

I'm new in reactive words, so, please help to find the best solution for this scenario:
I work with Youtube API. I want to load VideoCategories, then get one top video for each category, then load thumbnail for each video, accumulate it into model, and only then send signal to table view to reload data.
I request categories like this:
[[[TRYoutubeManager manager] rac_GET:#"videoCategories" parameters:parameters] map:^id(id responseObject) {
TRYoutubeListResponseModel *listModel =
[MTLJSONAdapter modelOfClass:[TRYoutubeListResponseModel class] fromJSONDictionary:responseObject error:nil];
listModel.items = [[listModel.items.rac_sequence filter:^BOOL(TRYoutubeVideoCategoryModel *categoryModel) {
return categoryModel.assignable;
}] array];
return listModel;
}];
So, how to send request for each listModel.items and then combine the result, and then signal the table view?
Ok, for everybody who still wonder. Explanation in more abstract way:
// You get your list ob objects here
[[[Manager getList] flattenMap:^RACStream *(NSArray *yourList) {
NSMutableArray *listItemsSignals = [NSMutableArray array];
for (ItemClass *item in yourList) {
//Something that produces signals
RACSignal *itemSignal = [item imageSignal];
[listItemsSignals addObject: itemSignal]
}
return [RACSignal combineLatest:listItemsSignals];
}] subscribeNext:^(RACTuple *values) {
// All your values are here
}];

parse call function in background messes up order sequence

I need to retrieve a couple of NSDicionaries that are compared against an id.
First, I'm calling a NSArray with these id's in them. I'm looping over them to see get the details of that id, and with that i'm calling another pfcloud function. Up until this point, all goes well. However, when I'm logging the payment details of the payment id's, the order sequence is is in a different order than the array I putted it in.
for(__block NSString *paymentId in success){
[self getPaymentDetails:paymentId];
}
So for instance: success = #[#"1",#"2",#"3"]
the method getPaymentDetails will log me#[#"details about 1", #"details about 3", #"details about 2"]
However, I need them to be in the exact same order.
This is my getPaymentDetails code:
-(void)getPaymentDetails:(NSString *)paymentId{
PFUser *currentUser = [PFUser currentUser];
[PFCloud callFunctionInBackground:#"getpaymentdetails"
withParameters:#{#"objectid": paymentId, #"userid": currentUser.objectId}
block:^(NSDictionary *success, NSError *error) {
if(success){
NSDictionary *payment = success;
NSString *amount = [payment objectForKey:#"amount"];
if (![amount isKindOfClass:[NSNull class]]) {
[self.amountArray addObject:amount];
}
else {
[self.amountArray addObject:#""];
}
NSString *from = [payment objectForKey:#"from"];
if (![from isKindOfClass:[NSNull class]]) {
[self.fromArray addObject:from];
}
else {
[self.fromArray addObject:#""];
}
} else{
NSLog(#"Error logged getpaymentdetails: %#", error);
}
}];
}
The values stored in the amountArray for instance, do not match the index of the paymentId
How come and how do I solve this?
It may be simpler to just move the whole for loop into the background and then call the Parse function synchronously
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group,queue, ^{
for(__block NSString *paymentId in success){
[self getPaymentDetails:paymentId];
}
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// Release the group when it is no longer needed.
dispatch_release(group);
Then in your getPaymentDetails you would call callFunction:withParameters:error: instead of callFunctionInBackground:withParameters:
This isn't an ideal solution however, as you are eliminating concurrency and so it will take longer to execute.
A better solution is to deal with the fact that the array is unordered at the conclusion of the loop and sort it once all of the data has been retrieved
The request callFunctionInBackground will do is executed asynchronously and there is no guarantee that the first call you make in your loop will finish first. This is not really related to Parse itself, that is just the nature of how this is done. You may end up with the same order by coincidence or a completely random one each time you execute this code.
If you want the order to stay the same, either pass in all IDs to your Cloud Function and update your Cloud Function to handle it or always wait for one call to finish, add the result to your array and then get the details with the next ID (basically a queue).

Resources