I am starting out on NSOperationQueue in iOS and facing some basic issues which I would like to clarify.
This is the code I am using:
-(void) SendRequestWithURL:(NSString*) URL andParam:(id) attributes {
[[AFNetworkActivityIndicatorManager sharedManager] incrementActivityCount];
_m_singleton = [Singleton sharedSingleton];
_parser = [[Syncparser alloc]init];
NSString *strServURL = [NSString stringWithFormat:#"%#%#",_m_singleton.globalstrURLLink,URL];
if (_theService == Item0 || _theService == Item1 || _theService == Item2){
NSLog(#"Entrance _theService %i", _theService);
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:strServURL]];
[client postPath:#"POST" parameters:attributes
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"_theService %i", _theService);
switch (_theService) {
case Item0: {
[_m_singleton.globalQueue addOperationWithBlock:^{
NSLog(#"ParseItem0 %#", [operation responseString]);
[_parser ParseItem0:[operation responseString]];
}];
}
break;
case Item1:{
[_m_singleton.globalQueue addOperationWithBlock:^{
NSLog(#"ParseItem1 %#", [operation responseString]);
[_parser ParseItem1:[operation responseString]];
}];
}
break;
case Item2:{
[_m_singleton.globalQueue addOperationWithBlock:^{
NSLog(#"ParseItem2 %#", [operation responseString]);
[_parser ParseItem2:[operation responseString]];
}];
}
break;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}
}
I only called item1 and item2.These are the values I get in the debugger.
Entrance _theService 1
Entrance _theService 2
_theService 2
_theService 1
This clearly means that the item2 gets added to the queue first and followed by item1.
But surprisingly, ParseItem1 gets called before ParseItem2 everytime. Even if _theService 1 comes before _theService 2. Not sure why. Sorry for the noob question.
Need some guidance.
Have you tried changing the priority of the NSOperation that you are adding?
The following are the possible values for the NSOperation priority property:
- NSOperationQueuePriorityVeryLow
- NSOperationQueuePriorityLow
- NSOperationQueuePriorityNormal
- NSOperationQueuePriorityHigh
- NSOperationQueuePriorityVeryHigh
You might also want to set the maxConcurrentOperationCount property to 1 if you want it to process one operation at a time.
Another is you can set the dependency property of an NSOperation of _theService1 with the value _theService2 so that _theService2 will have to wait for _theService1 to finish before it executes.
This clearly means that the item2 gets added to the queue first and
followed by item1.
This assumption is wrong. NSOperationQueue, used by AFNetworking, does not guarantee that the operations will be started or finished in the order you add them. In face, in AFHTTPClient's init method, you can see:
self.operationQueue = [[NSOperationQueue alloc] init];
[self.operationQueue setMaxConcurrentOperationCount:NSOperationQueueDefaultMaxConcurrentOperationCount];
So your queue is going to be executing operations concurrently for sure.
I can't say why this reverse call is always happening in your testing, but I assume the second call is either faster to return, or faster to parse, or both.
Anyway, if you need both operations to return to parse successfully, you can use enqueueBatchOfHTTPRequestOperations:progressBlock:completionBlock: and the completion block won't get called until they're all done. Or you can set one operation to be dependant on another (since it's an NSOperation subclass.)
Related
- (void)loadItems {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
[manager GET:#"someurl"
parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self reloadData];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems];
}
I am trying to implement instant search by making an API call every time a character changes. Since, the first few calls have less letters, they return more results, making the first few async calls finish slower than than the last few, meaning that if I type in hello quickly, I will end up getting the search results for h instead of the whole word since the last call to finish is the one for h. I need to keep the order of these calls, and make sure that the last query is not overwritten. I understand that I must use a queue structure. However doing something like this in textFieldDidChange doesn't seem to work:
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
[self loadItems];
});
dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
[self reloadData];
});
I think I need to use some sort of combination of dispatch_group_enter(group); and dispatch_group_leave(group);. However I still can't get the calls to stop overwriting the last call. I'm not sure if there is also a way to just cancel out all the other started calls with the last one, or if I have to wait for all of them to finish in order. Any help would be appreciated.
This was my solution. I just ended up using a counter that I pass into my loadItems function. While that counter updates, the async call still has its own value in it, so I just compare the two, and make sure to only reloadData if the async call's counter is equal to the latest one.
- (void)loadItems:(int)queryInt {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
[manager GET:#"someurl"
parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (searchQueryCounter - 1 == queryInt) {
[self reloadDatawithAnimation];
} else {
return;
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems:searchQueryCounter];
searchQueryCounter = searchQueryCounter + 1;
}
You might better address this by canceling the prior requests, not only preventing prior requests reporting results, but also ensuring that system resources are not consumed by requests that are no longer needed:
#interface ViewController () <UITextFieldDelegate>
#property (nonatomic, strong) AFHTTPRequestOperationManager *manager;
#property (nonatomic, weak) NSOperation *previousOperation;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// no need to instantiate new request operation manager each time;
// do it at some logical point of initialization (e.g. in `viewDidLoad`
// for view controllers, etc.).
self.manager = [AFHTTPRequestOperationManager manager];
[self.manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
}
- (void)loadItems {
[self.previousOperation cancel];
typeof(self) __weak weakSelf = self; // probably should use weakSelf pattern, too
NSOperation *operation = [self.manager GET:#"someurl" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[weakSelf reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if ([error.domain isEqualToString:NSURLErrorDomain] && [error code] != NSURLErrorCancelled) {
NSLog(#"Error: %#", error);
}
}];
self.previousOperation = operation;
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems];
}
#end
I actually worked on a very similar problem last week and came up with an approach you might find useful.
I submit each request using performSelector:withObject:afterDelay with a slight delay (I've been experimenting with values ranging from 0.5 to 1.0 seconds. Somewhere from .66 to .75 seems like a good compromise value.)
With each new request, I cancel the previous pending performSelector call. That way nothing gets sent until the user stops typing for a short period of time. It's not perfect, but it reduces the amount of useless queries for word fragments. The code looks something like this:
static NSString *methodWord = nil;
[[self class] cancelPreviousPerformRequestsWithTarget: self
selector: #selector(handleWordEntered:)
object: methodWord];
methodWord = word;
[self performSelector: #selector(handleWordEntered:)
withObject: methodWord
afterDelay: .667];
The method handleWordEntered: actually sends the request to the server.
If the user types a letter, then another letter in less than 2/3 second, the previous pending request is cancelled and a new request is set to fire 2/3 of a second later. As long as the user keeps typing letters every 2/3 second, nothing is sent. As soon as the user pauses more than 2/3 second, a request is sent. Once the performSelector:withObject:afterDelay fires it can't be cancelled any more, so that request goes to the network and the reply is parsed.
I have a unit test in which I need to wait for an async task to finish. I am trying to use NSConditionLock as it seems to be a pretty clean solution but I cannot get it to work.
Some test code:
- (void)testSuccess
{
loginLock = [[NSConditionLock alloc] init];
Login login = [[Login alloc] init];
login.delegate = self;
// The login method will make an async call.
// I have setup myself as the delegate.
// I would like to wait to the delegate method to get called
// before my test finishes
[login login];
// try to lock to wait for delegate to get called
[loginLock lockWhenCondition:1];
// At this point I can do some verification
NSLog(#"Done running login test");
}
// delegate method that gets called after login success
- (void) loginSuccess {
NSLog(#"login success");
// Cool the delegate was called this should let the test continue
[loginLock unlockWithCondition:1];
}
I was trying to follow the solution here:
How to unit test asynchronous APIs?
My delegate never gets called if I lock. If I take out the lock code and put in a simple timer it works fine.
Am I locking the entire thread and not letting the login code run and actually make the async call?
I also tried this to put the login call on a different thread so it does not get locked.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[login login];
});
What am I doing wrong?
EDIT adding login code. Trimmed do the code for readability sake. Basically just use AFNetworking to execute a POST. When done will call delegate methods.
Login make a http request:
NSString *url = [NSString stringWithFormat:#"%#/%#", [_baseURL absoluteString], #"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (_delegate) {
[_delegate loginSuccess];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (_delegate) {
[_delegate loginFailure];
}
}];
The answer can be found in https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFHTTPRequestOperation.m.
Since you are not setting the completionQueue property of the implicitly created AFHTTPRequestOperation, it is scheduling the callbacks on the main queue, which you are blocking.
Unfortunately, many answers (not all) in the given SO thread ("How to unit test asynchronous APIs?") are bogus and contain subtle issues. Most authors don't care about thread-safity, the need for memory-barriers when accessing shared variables, and how run loops do work actually. In effect, this leads to unreliable and ineffective code.
In your example, the culprit is likely, that your delegate methods are dispatched on the main thread. Since you are waiting on the condition lock on the main thread as well, this leads to a dead lock. One thing, the most accepted answer that suggests this solution does not mention at all.
A possible solution:
First, change your login method so that it has a proper completion handler parameter, which a call-site can set in order to figure that the login process is complete:
typedef void (^void)(completion_t)(id result, NSError* error);
- (void) loginWithCompletion:(completion_t)completion;
After your Edit:
You could implement your login method as follows:
- (void) loginWithCompletion:(completion_t)completion
{
NSString *url = [NSString stringWithFormat:#"%#/%#", [_baseURL absoluteString], #"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (completion) {
completion(responseObject, nil);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (completion) {
completion(nil, error);
}
}];
Possible usage:
[self loginWithCompletion:^(id result, NSError* error){
if (error) {
[_delegate loginFailure:error];
}
else {
// Login succeeded with "result"
[_delegate loginSuccess];
}
}];
Now, you have an actual method which you can test. Not actually sure WHAT you are trying to test, but for example:
-(void) testLoginController {
// setup Network MOCK and/or loginController so that it fails:
...
[loginController loginWithCompletion:^(id result, NSError*error){
XCTAssertNotNil(error, #"");
XCTAssert(...);
<signal completion>
}];
<wait on the run loop until completion>
// Test possible side effects:
XCTAssert(loginController.isLoggedIn == NO, #""):
}
For any other further steps, this may help:
If you don't mind to utilize a third party framework, you can then implement the <signal completion> and <wait on the run loop until completion> tasks and other things as described here in this answer: Unit testing Parse framework iOS
My assumption is that the operations are running asynchronously on a separate thread, but the loop never exits, so something is not as I assumed.
/**
Checks if we can communicate with the APIs
#result YES if the network is available and all of the registered APIs are responsive
*/
- (BOOL)apisAvailable
{
// Check network reachability
if (!_connectionAvailable) {
return NO;
}
// Check API server response
NSMutableSet *activeOperations = [[NSMutableSet alloc] init];
__block NSInteger successfulRequests = 0;
__block NSInteger failedRequests = 0;
for (AFHTTPClient *httpClient in _httpClients) {
// Send heart beat request
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:#"" parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Server returned good response
successfulRequests += 1;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Server returned bad response
failedRequests += 1;
}];
[operation start];
[activeOperations addObject:operation];
}
// Wait for heart beat requests to finish
while (_httpClients.count > (successfulRequests + failedRequests)) {
// Wait for each operation to finish, one at a time
//usleep(150);
[NSThread sleepForTimeInterval:0.150];
}
// Check final results
if (failedRequests > 0) {
return NO;
}
return YES;
}
A few suggestions:
Never check reachability to determine if a request will succeed. You should try the request; only if it fails should you consult reachability to try and get a best guess as to why. Reachability makes no guarantee about whether a request will fail or succeed.
Is this method called on the main thread? Even if you fixed the problem with the requests never completing, it will block the UI the entire time your network requests are running. Since these requests can take potentially a long time, this is a bad experience for the user as well as something the OS will kill your app for if it happens at the wrong time (e.g. at launch).
Looping while calling sleep or equivalent is wasteful of CPU resources and memory, as well as prevents the thread's runloop from servicing any timers, event handler or callbacks. (Which is probably why the networking completion blocks never get to run.) If you can avoid blocking a thread, you should. In addition, Cocoa will very often be unhappy if you do this on an NSThread you didn't create yourself.
I see two options:
Use dispatch_groups to wait for all of your requests to finish. Instead of blocking your calling thread, you should instead take a completion block to call when you're done. So, instead of returning a BOOL, take a completion block which takes a BOOL. Something like - (void)determineIfAPIIsAvailable:(void(^)(BOOL))completionBlock;
Get rid of this method altogether. What are you using this method for? It's almost certainly a better idea to just try to use your API and report appropriate errors to the user when things fail rather than to try to guess if a request to the API will succeed beforehand.
I believe the issue is that I was not using locking to increment the counters so the while loop would never evaluate to true.
I was able to get it working by only looking for a fail count greater than 0 that way as long as it was incremented by any of the request callback blocks then I know what to do.
I just so happen to have switched to [NSOperationQueue waitUntilAllOperationsAreFinished].
Final code:
/**
Checks if we can communicate with the APIs
#result YES if the network is available and all of the registered APIs are responsive
*/
- (BOOL)apisAvailable
{
// Check network reachability
if (!_connectionAvailable) {
return NO;
}
// Check API server response
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
__block NSInteger failedRequests = 0;
for (AFHTTPClient *httpClient in _httpClients) {
// Send heart beat request
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:#"" parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Server returned good response
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Server returned bad response
failedRequests += 1;
}];
[operationQueue addOperation:operation];
}
// Wait for heart beat requests to finish
[operationQueue waitUntilAllOperationsAreFinished];
// Check final results
if (failedRequests > 0) {
return NO;
}
return YES;
}
I am developing an app and when it starts its execution it has to get some data from the webService, categories, Image of loading(it changes sometimes), info "how to use" ( also can change in the server, client specifications..). To get this data I call some methods like this one (I have four similar methods, one for each thing I need) :
-(void) loadAppInfo
{
__weak typeof(self) weakSelf = self;
completionBlock = ^(BOOL error, NSError* aError) {
if (error) {
// Lo que sea si falla..
}
[weakSelf.view hideToastActivity];
};
[self.view makeToastActivity];
[wpNetManager getApplicationInfoWithCompletionBlock:completionBlock];
}
In my Network manager I have methods like this one :
- (void)getApplicationInfoWithCompletionBlock:(CompletionBlock)completionBlock
{
NSString * lang = #"es";//[[NSLocale preferredLanguages] objectAtIndex:0];
NSString *urlWithString = [kAPIInfoScreens stringByAppendingString:lang];
NSMutableURLRequest *request = nil;
request = [self requestWithMethod:#"GET" path:urlWithString parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[self registerHTTPOperationClass:[AFHTTPRequestOperation class]];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Print the response body in text
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseObject options:kNilOptions error:nil];
NSDictionary *informations = [json objectForKey:kTagInfoSplash];
if([json count]!= 0){
for (NSDictionary *infoDic in informations) {
Info *info = [Info getInfoByTitle:[infoDic objectForKey:kTagInfoTitle]];
if (info) {
// [User updateUserWithDictionary:dic];
} else {
[Info insertInfoWithDictionary:infoDic];
}
}
[wpCoreDataManager saveContext];
}
if (completionBlock) {
completionBlock(NO, nil);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error Registro: %#", error);
if (completionBlock) {
completionBlock(YES, error);
}
}];
[self enqueueHTTPRequestOperation:operation];
}
So what I do is call this methods in the viewDidLoad:
[self loadAppInfo];
[self loadCountriesFromJson];
[self loadCategoriesFromWS];
[self loadSplashFromWS];
So, instead of call this methods one by one. I think I can use GCD to manage this while a load image is called until everything is done and then call the next ViewController. It is a good solution what I believe? if it is the problem is that I do not know how to add some blocks to a gcd.
I am trying to do this instead of calling he last four methods in ViewDidLoad. But it works weird:
-(void)myBackGroundTask
{
[self.view makeToastActivity];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self loadAppInfo];
[self loadCountriesFromJson];
[self loadCategoriesFromWS];
[self loadSplashDataFromWS ];
dispatch_async(dispatch_get_main_queue(), ^{
[self.view hideToastActivity];
[self nextController];
});
});
}
[self nextController] method is called before I had everything save in Core Data and I have errors..
Since all your four methods
[self loadAppInfo];
[self loadCountriesFromJson];
[self loadCategoriesFromWS];
[self loadSplashFromWS];
are asynchronous, it should be clear why the statement
[self nextController];
is executed before those four methods finish. Right?
Thus, there are completion handlers which get invoked when the asynchronous method finished. Too bad, none of your asynchronous methods have completion handlers. ;)
The key to approach the problem seems to have completion handlers for your asynchronous methods:
typedef void (^completion_t)(id result, NSError* error);
- (void) loadAppInfo:(completion_t)completionHandler;
- (void) loadCountriesFromJson:(completion_t)completionHandler;
- (void) loadCategoriesFromWS:(completion_t)completionHandler;
- (void) loadSplashFromWS:(completion_t)completionHandler;
It seems, you want to start ALL four asynchronous methods concurrently.
How and when you have to invoke the statement [self nextController] depends on whether there are any dependencies for this call to the eventual result of the above four asynchronous methods.
For example, you may state:
A. [self nextController] shall be executed when loadAppInfo: finishes successfully. All other asynchronous methods are irrelevant.
The solution looks like this:
[self loadAppInfo:^(id result, NSError*error){
if (error == nil) {
[self nextController];
}
}];
[self loadCountriesFromJson:nil];
[self loadCategoriesFromWS:nil];
[self loadSplashFromWS:nil];
If the above statement depends only on one of those methods, the solution is quite obvious and simple. It will get immediately more complex when you have a requirement like this:
B. [self nextController] shall be executed when ALL four asynchronous methods finished successfully (or more than one, and all other methods are irrelevant).
There are a few approaches how one can solve that. One would be to use a dispatch group, or a semaphore and a few state variables and dispatch queues to ensure concurrency. However, this is quite elaborate, would ultimately cause to block a thread, cannot be cancelled, and is also quite suboptimal (besides that it also looks hackish). Thus, I will not discuss that solution.
Using NSOperation and Dependencies
Another approach is to utilize NSOperation's dependencies. This requires to wrap each asynchronous method into a NSOperation subclass. Your methods are already asynchronous, this means that you need to take this into account when designing your subclasses.
Since one can only establish a dependency from one to another NSOperation, you also need to create a NSOperation subclass for your statement
[self nextController]
which needs to be wrapped into its own NSOperation subclass.
Well assuming you correctly subclassed NSOperation, at the end of the day, you get five modules and five header files:
LoadAppInfoOperation.h, LoadAppInfoOperation.m,
LoadCountriesFromJsonOperation.h, LoadCountriesFromJsonOperation.m,
LoadCategoriesFromWSOperation.h, LoadCategoriesFromWSOperation.m,
LoadSplashFromWSOperation.h, LoadSplashFromWSOperation.m
NextControllerOperation.h, NextControllerOperation.m
B. NextControllerOperation shall be started when ALL four Operations finished successfully:
In code this looks as follows:
LoadAppInfoOperation* op1 = ...;
LoadCountriesFromJsonOperation* op2 = ...;
LoadCategoriesFromWSOperation* op3 = ...;
LoadSplashFromWSOperation* op4 = ...;
NextControllerOperation* controllerOp = ...;
[controllerOp addDependency:op1];
[controllerOp addDependency:op2];
[controllerOp addDependency:op3];
[controllerOp addDependency:op4];
NSOperationQueue *queue = [NSOperationQueue new];
[queue addOperation: op1];
[queue addOperation: op2];
[queue addOperation: op3];
[queue addOperation: op4];
[queue addOperation: controllerOp];
Looks nice? No?
A more appealing approach: Promises
If this solution with NSOperations doesn't look nice, is too elaborated (five NSOperation subclasses!) or whatever, here is a more appealing approach which uses a third party library which implements Promises.
Before I explain how Promises work and what they are for (see wiki for a more general description), I would like to show the final code right now here, and explain how to get there later.
Disclosure: The example code here utilizes a third party library RXPromise which implements a Promise according the Promise/A+ specification. I'm the author of the RXPromise library.
There are a few more Promise libraries implemented in Objective-C, but you may take a look into RXPromise anyway ;) (see below for a link)
The key is to create asynchronous methods which return a promise. Assuming ALL your methods are now asynchronous and have a signature like below:
- (RXPromise*) doSomethingAsync;
Then, your final code will look as follows:
// Create an array of promises, representing the eventual result of each task:
NSArray* allTasks = #[
[self loadAppInfo],
[self loadCountriesFromJson],
[self loadCategoriesFromWS],
[self loadSplashFromWS]
];
...
This above statement is a quite a short form of starting a number of tasks and holding their result objects (a promise) in an array. In other words, the array allTasks contains promises whose task has been started and which now run all concurrently.
Now, we continue and define what shall happen when all tasks within this array finished successfully, or when any tasks fails. Here we use the helper class method all::
...
[RXPromise all: allTasks]
.then(^id(id results){
// Success handler block
// Parameter results is an array of the eventual result
// of each task - in the same order
... // do something with the results
return nil;
},^id(NSError*error){
// Error handler block
// error is the error of the failed task
NSLog(#"Error: %#, error");
return nil;
});
See the comments in the code above to get an idea how the success and the error handler - which get called when all tasks have been finished - is defined with the "obscure" then.
The explanation follows:
Explanation:
The code below uses the RXPromise library. You can obtain the source code of RXPromise Library which is available at GitHub.
There are a few other implementations (SHXPromise, OMPromises and more) and with a little effort it should be possible to port the code below to other promise libraries as well.
First, you need a variant of your asynchronous methods which looks as follows:
- (RXPromise*) loadAppInfo;
- (RXPromise*) loadCountriesFromJson;
- (RXPromise*) loadCategoriesFromWS;
- (RXPromise*) loadSplashFromWS;
Here, note that the asynchronous methods don't have a completion handler. We don't need this since the returned object -- a Promise -- represents the eventual result of the asynchronous task. This result may also be an error when the task fails.
I've refactored your original methods in order to better utilize the power of promises:
An asynchronous task will create the promise, and it must eventually "resolve" it either with the eventual result via fulfillWithValue:, or when it fails, with an error via rejectWithReason:. See below how a RXPromise is created, immediately returned from the asynchronous method, and "resolved" later when the task finished or failed.
Here, your method getApplicationInfo returns a promise whose eventual value will be the HTTP response data, that is a NSData containing JSON (or possibly an error):
- (RXPromise*)getApplicationInfo
{
RXPromise* promise = [[RXPromise alloc] init];
NSString * lang = #"es";//[[NSLocale preferredLanguages] objectAtIndex:0];
NSString *urlWithString = [kAPIInfoScreens stringByAppendingString:lang];
NSMutableURLRequest *request = nil;
request = [self requestWithMethod:#"GET" path:urlWithString parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[self registerHTTPOperationClass:[AFHTTPRequestOperation class]];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
[promise fulfillWithValue:responseObject]
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[promise rejectWithReason:error];
}];
[self enqueueHTTPRequestOperation:operation];
return promise;
}
A few further notes about promises:
A client can obtain the eventual result respectively the error through registering handler blocks through using the property then:
promise.then(<success_handler>, <error_handler>);
Handlers or optional, but you usually set either one or both which handle the result.
Note: With RXPromise you can register handler blocks when and where you want, and as many as you want! RXPromise is fully thread safe. You just need to keep a strong reference to the promise somewhere or as long as needed. You don't need to keep a reference, even when you setup handlers, though.
The handler block will be executed on a private queue. This means, you don't know the execution context aka thread where the handler will be executed, except you use this variant:
promise.thenOn(dispatch_queue, <success_handler>, <error_handler>);
Here, dispatch_queue specifies the queue where the handler (either the success OR the error handler) will be executed.
Two or more asynchronous tasks can be executed subsequently (aka chained), where each task produces a result which becomes the input of the subsequent task.
A short form of "chaining" of two async methods looks like this:
RXPromise* finalResult = [self asyncA]
.then(^id(id result){
return [self asyncBWithResult:result]
}, nil);
Here, asyncBWithResult: will be executed only until after asyncA has been finished successfully. The above expression returns a Promise finalResult which represents the final result of what asyncBWithResult: "returns" as its result when it finishes, or it contains an error from any task that fails in the chain.
Back to your problem:
Your method loadAppInfo now invokes asynchronous method getApplicationInfo in order to obtain the JSON data. When that succeeded, it parsers it, creates managed objects from it and saves the managed object context.
It returns a promise whose value is the managed object context where the objects have been saved:
- (RXPromise*) loadAppInfo {
RXPromise* promise = [[RXPromise alloc] init];
[self getApplicationInfo]
.then(^(id responseObject){
NSError* err;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseObject options:kNilOptions error:&err];
if (json == nil) {
return err;
}
else {
[wpCoreDataManager.managedObjectContext performBlock:^{
NSDictionary *informations = [json objectForKey:kTagInfoSplash];
if([json count]!= 0){
for (NSDictionary *infoDic in informations) {
Info *info = [Info getInfoByTitle:[infoDic objectForKey:kTagInfoTitle]];
if (info) {
// [User updateUserWithDictionary:dic];
} else {
[Info insertInfoWithDictionary:infoDic];
}
}
[wpCoreDataManager saveContext]; // check error here!
[promise fulfillWithValue:wpCoreDataManager.managedObjectContext];
}
else {
[promise fulfillWithValue:nil]; // nothing saved
}
}];
}
}, nil);
return promise;
}
Notice how performBlock has been utilized to ensure the managed objects are properly associated to the execution context of its managed object context. Additionally, the asynchronous version is used, which fits nicely into the solution utilizing promises.
Having refactored these two methods, which merely perform what you intend to accomplish, and also having refactored the other asynchronous methods which now return a promise like the refactored above methods, you can now finish your task as shown at the start.
GCD to manage this while a load image is called until everything is done and then call the next ViewController. It is a good solution what I believe?
The general rule of thumb is to operate on the highest level of abstraction available.
In this case it means using NSOperation subclasses. You can create a private queue, and schedule you operations in such a way that turning off the loading image will happen only after all operations are complete, e.g. by
NSOperation *goForward = [MyGoForwardOperation new]; // you define this subclass
NSOperation *loadSomething = [MyLoadSomethingOperation new];
NSOperation *loadAnother = [MyLoadAnotherThingOperation new];
[goForward addDependency: loadOperation];
[goForward addDependency: loadAnother];
NSOperationQueue *queue = [NSOperationQueue new];
[queue addOperation: loadSomething];
[queue addOperation: loadAnother];
[[NSOperationQueue mainQueue] addOperation: goForward];
Note that in this example goForward will run on main thread, but after background operations finish.
You'll need to carefully program your MyLoadSomethingOperation for this to work, read up on subclassing NSOperation or subclass AFHTTPRequestOperation since you're using it anyway.
[self nextController] method is called before I had everything
Yes, you should search on saving to Core Data on background thread; this is a big topic in itself.
At first
I have this
ZTCAPIClient *api = [ZTCAPIClient sharedClient];
__block BOOL sessionSuccess = NO;
//Get session
[api getPath:#"api-getsessionid.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id JSON) {
NSMutableDictionary *dict = [self dealWithZTStrangeJSON:JSON];
if ([dict count]) {
NSLog(..something..);
sessionSuccess = YES;
NSLog(#"inside:%u",sessionSuccess);
} else {
NSLog(#"ERROR: Get no session!");
sessionSuccess = NO;
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"ERROR: %#",error);
sessionSuccess = NO;
}];
[api.operationQueue waitUntilAllOperationsAreFinished];
NSLog(#"outside:%u",sessionSuccess);
but I will get:
outside:0
inside:1
I know it's the async reason.
So I searched on the Internet, then I found this: wait until multiple operations executed - including completion block (AFNetworking)
So I try it:
ZTCAPIClient *api = [ZTCAPIClient sharedClient];
__block BOOL sessionSuccess = NO;
dispatch_group_t group = dispatch_group_create();
//Get session
dispatch_group_enter(group);
[api getPath:#"api-getsessionid.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id JSON) {
NSMutableDictionary *dict = [self dealWithZTStrangeJSON:JSON];
if ([dict count]) {
NSLog(..something..);
sessionSuccess = YES;
NSLog(#"inside:%u",sessionSuccess);
} else {
NSLog(#"ERROR: Get no session!");
sessionSuccess = NO;
}
dispatch_group_leave(group);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"ERROR: %#",error);
sessionSuccess = NO;
dispatch_group_leave(group);
}];
//[api.operationQueue waitUntilAllOperationsAreFinished];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
DLog(#"outside:%u",sessionSuccess);
then I get nothing...
nothing output.
Where is wrong?
You're probably not getting any output because your program is never moving past the call to dispatch_group_wait. If it did, then you'd see the "outside" log statement.
If dispatch_group_wait is never returning, then there must still be something in the group. In your sample code, you add one thing to the group with dispatch_group_enter and then remove it in either the success or failure handler for the api call with dispatch_group_leave. This means that dispatch_group_leave is not being called for some reason.
My suspicion is that the reason the blocks are not being called is that they will be invoked asynchronously on the same dispatch queue that your outer code runs on. If this is the case, then they can't run until dispatch_group_wait returns and dispatch_group_wait cannot return until the blocks run. This is called deadlock. (Edit: Alternatively, it could be that some part of the program that invokes the success or failure blocks is the part that is leading to deadlock. Either way, the result is that the blocks can't get called since dispatch_group_wait never returns.)
The other possibility is that the method -dealWithZTStrangeJSON: never returns for some reason. If this is the case, then the success block will be invoked (you could set a breakpoint on its first line to verify), but it will never make it to dispatch_group_leave.
In either case, I would recommend that you think about solving your problem another way instead of waiting for the operation to finish. Perhaps you can do the things that you were planning to do after dispatch_group_wait returns inside of the success handler instead (or another way of thinking about it would be that the success or failure handler could call a method that does the things you're currently doing after dispatch_group_wait—either way will work, but sometimes I find that it's easier to keep my code organized by calling out to a method instead of putting all of the code in a block. This could be especially useful if you want to share some code between the success and failure blocks).