The best practice to handle chaining requests using reactive cocoa - ios

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

Related

how to wait until the task is completed and then return a signal, reactive cocoa

-(RACSignal*)finalPackage {
RACSignal *endPoint = [[DGConfiguration sharedInstance].apiConfiguration
urlTemplate:DGAPIUrlLocalWatchList];` // 1.
return [[endPointRequestSignal map:^id(NSString *endPoint) { // 2.
return service([NSURL URLWithString: endPoint]);
}].flatten map:^id(NSArray *episodes) { // 3.
NSMutableArray *info= [NSMutableArray array];
__block NSArray *result=#[#(9)]; // test value is 9, result will be updated during callback block
[episodes enumerateObjectsUsingBlock:^(NSDictionary *item, NSUInteger idx, BOOL *stop) {
[info addObject:#{#"id":item[#"id"],#"links":item[#"links"]}];
}];
[[DGManager sharedInstance] updateVideoStateWith:info callback:^(NSArray *response) { // 4.
dispatch_async(dispatch_get_main_queue(), ^{
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"timeStamp" ascending:NO];
result = [[response sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]] copy];
});
}];
return [RACSignal return:result]; // 5.
}].flatten;
}
Lets me explain what I am trying to do.
I wrap the endPoint url via endPoint signal
Using map to extract url and do a service call (service([NSURL URLWithString: endPoint]))
Using map to extract info from step 2 and create info data
Do updateVideoStateWith with a callback
Return a signal which contains result
Eventually, when I subcribe to finalPackage signal, the return is initialized value which is 9.I realize that the updateVideoStateWith call back will take time to return the result.
My question is how can I force return [RACSignal return:result] wait until the data is updated from callback block. I did tried takeUntilBlock but not sure how to use it. I also think about using switchToLatest but still no luck.
Cross posting my answer from the GitHub issue:
- (RACSignal*)finalPackage {
return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
RACSignal *endPointSignal = [[DGConfiguration sharedInstance].apiConfiguration urlTemplate:DGAPIUrlLocalWatchList];
[[endPointSignal map:^id(NSString *endPoint) {
// map your endpoints to episodes and return the array of episodes
}] subscribeNext:^(NSArray* episodes) {
// Create your initial result array
[[DGManager sharedInstance] updateVideoStateWith:info callback:^(NSArray *response) {
// Do whatever work you need to do with the response to modify the result array
[subscriber sendNext:result];
[subscriber sendComplete];
}];
} error:^(NSError* error) {
[subscriber sendError:error];
]];
return nil;
}];
}
Note: if you're returning a RACSignal* when mapping from the endPoint NSString you'll want to flattenMap instead of map, flattenMap will flatten out the signal that is returned into the value it emits.

How to Accomplish this w/ ReactiveCocoa

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

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.

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).

Reactive Cocoa: Response from HTTPRequest using RACCommand

I'm completely new to the Reactive Cocoa Framework and i'm just doing some simple tests but i'm encountering a problem that i would like to understand.
Basically i'm just doing an API call to fetch a JSON object from my server, i want to do this with RAC. So my steps are the following:
First i build the RACCommand this way:
RACCommand *getLatestVersionCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[[API sharedInstance] getLatestAppVersion]
materialize];
}];
Now i create the signal and subscribe to it this way:
RACSignal *versionCodeSignal = [[getLatestVersionCommand.executionSignals flatten] deliverOn:[RACScheduler mainThreadScheduler]];
[[versionCodeSignal
map:^id(NSDictionary *responseObject) {
return responseObject;
}]
subscribeNext:^(NSDictionary *responseObject) {
NSArray *allVersions = [[NSArray alloc] initWithArray:[responseObject objectForKey:KEY_VERSIONS]];
for(NSDictionary *version in allVersions) {
NSString *device = [version objectForKey:KEY_DEVICE];
if([device isEqualToString:KEY_IOS]) {
NSString *latestVersionName = [[version objectForKey:KEY_VERSION] objectForKey:KEY_NAME];
if([APP_VERSION compare:latestVersionName options:NSNumericSearch] == NSOrderedAscending) {
//There is a new version!
NSLog(#"There is a new version!!!");
}
}
}
}];
Finally I execute the command this way
[getLatestVersionCommand execute:self];
The problem i'm facing is that in the subscribeNext block, the object i receive is an RACEvent object and not the dictionary i'm expecting. I know i'm doing something wrong and not understanding the full flow correctly, i tried adding the map function that i thought i didn't need just to test and nothing.
The only way i found is to convert the response in the map block to a RACEvent and return its value but that doesn't seem to me the right way to go.
Any light will be greatly appreciated.
Remove the call to -[RACSignal materialize] from your command's signal block.

Resources