I have a simple app which try to login users.
User insert username and password
I call a method in another class ->
if ([myBankLogger checkUserLogin:self.memberNumber.text :self.accessCode.text])
{
//Check if user credential is correct
NSLog(#"Corrct");
}
else
{
NSLog(#"Not correct");
}
In checkUserLogin I send a http request to server and it takes a minute to get respond:
-(bool) checkUserLogin :(NSString*)username :(NSString*)password
{
__block bool tmp= false;
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager POST:string parameters:parameters progress:nil success:^(NSURLSessionTask *task, id responseObject) {
NSLog(#"JSON: %#", responseObject);
if (responseObject[#"secret_token"])
{
NSLog(#"HERE");
tmp = true;
}
} failure:^(NSURLSessionTask *operation, NSError *error) {
NSLog(#"Error: %#", error);
tmp = false;
}];
return tmp;
}
As it is normal compiler dose not wait until it finish processing. It returns false. I researched and found out I must use this:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
// Perform async operation
// Call your method/function here
// Example:
// NSString *result = [anObject calculateSomething];
dispatch_sync(dispatch_get_main_queue(), ^{
// Update UI
// Example:
// self.myLabel.text = result;
});
});
But I am not sure where I should put my code? Appreciate any suggestion or easier solution.
If you perform a synchronous operation on the main thread, this will block the app UI for the entire duration of that sync operation. If it takes too much, the OS will kill the application execution. With an async approach using the dispatch method, you can solve the UI blocking problem:
[self.myRingLoading startAnimating]; // show a ring loading while the following long operation will take (here you are in the main thread scope)
// this dispatch block will move all the actions in an async thread, avoiding any UI blocking issue
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
// Perform here your UI blocking operation
// sample blocking ui operation
NSData* httpDataResult = [NSData dataWithContentsOfURL:#"http://api.yoursite.com/xyz"];
NSString* httpStringResult = [[NSString alloc] initWithData:httpDataResult encoding:NSUTF8StringEncoding];
// this dispatch block will send all action execution to the main queue, commonly the main thread
dispatch_async(dispatch_get_main_queue(), ^(void){
// Perform your UI operation (eg: printing the http response received by the server
[self.myUILabel setText:httpStringResult];
[self.myRingLoading stopAnimating]; // hide the ring loading since the long operation is finished
});
});
There are other native solutions for obtaining the result, for example using the NSOperation and NSOperationQueue. If you use AFNetworking, you can take advantage of several good implementations, for example AFHTTPRequestOperation:
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc]initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Perform your UI actions for the Success case
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Perform your UI actions for the Failed case
}
];
// run the http operation process
[operation start];
Hope it helps
Related
Two concurrent background tasks need to patch two separate arrays which need to be merged in a dispatch_group_notify block.The problem is that.the first block is exceeded but the dispatch_group_notify is exceeded without waiting for the execution of the second background task.
The only different between them is that the first one make a local search and the second one makes a remote call to a web service.Any clue why the second one is jumped over ?
Edit: I also tried the approach mentioned in https://stackoverflow.com/a/19580584/859742 using dispatch_barrier_async but still same.
dispatch_group_t taskGroup = dispatch_group_create();
dispatch_queue_t mainQueue = dispatch_get_main_queue();
__block NSArray *localAddresses;
__block NSArray *remoteAddresses;
//Get local array in the background
dispatch_group_async(taskGroup, mainQueue, ^{
//localAddresses is set
});
//get remote array from server
dispatch_group_async(taskGroup, mainQueue, ^{
[[MDAddressManager instance] searchForPlacesContainingText:query
location:alocation
completion:^(NSArray* addresses, MDError *error){
//remoteAddresses is set
});
//Merge two arrays
dispatch_group_notify(taskGroup, mainQueue, ^{
//remoteAddresses and local addresses are merged
});
And the remote search method looks like this
- (void)searchForPlacesContainingText:(NSString *)searchText
location:(CLLocation *)alocation
completion:(MDAddressManagerBlock)completionBlock
{
NSDictionary *parameters = [[NSMutableDictionary alloc] init];
[parameters setValue:searchText forKey:#"input"];
[[MDHTTPClient sharedHTTPClient] getPath:#"v1/remotePlaces.json"
parameters:parameters
success:^(AFHTTPRequestOperation *operation, id dict) {
if ([MDHTTPClient isResponseValid:dict])
{
completionBlock(returnArray, nil);
}
else
{
completionBlock(nil, nil);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
EDLog(#"%#", error);
completionBlock(nil, [MDError errorAFNetworking:error]);
}];
}
This is because your getPath method runs asynchronously. You need it to not leave the group until that completion block runs. So rather than than doing dispatch_group_async, you should manually dispatch_group_enter and dispatch_group_leave.
You can change your code from:
dispatch_group_async(taskGroup, mainQueue, ^{
[[MDAddressManager instance] searchForPlacesContainingText:query
location:alocation
completion:^(NSArray* addresses, MDError *error){
//remoteAddresses is set
}];
});
To:
dispatch_group_enter(taskGroup);
[[MDAddressManager instance] searchForPlacesContainingText:query
location:alocation
completion:^(NSArray* addresses, MDError *error){
//remoteAddresses is set
dispatch_group_leave(taskGroup);
});
That will ensure that you don't leave the group until the completion block is called.
Alternatively, you could change searchForPlacesContainingText to use dispatch_group_t parameter:
- (void)searchForPlacesContainingText:(NSString *)searchText
location:(CLLocation *)alocation
group:(dispatch_group_t)group
completion:(MDAddressManagerBlock)completionBlock
{
dispatch_group_enter(group);
NSDictionary *parameters = [[NSMutableDictionary alloc] init];
[parameters setValue:searchText forKey:#"input"];
[[MDHTTPClient sharedHTTPClient] getPath:#"v1/remotePlaces.json"
parameters:parameters
success:^(AFHTTPRequestOperation *operation, id dict) {
if ([MDHTTPClient isResponseValid:dict])
{
completionBlock(returnArray, nil);
}
else
{
completionBlock(nil, nil);
}
dispatch_group_leave(group);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
EDLog(#"%#", error);
completionBlock(nil, [MDError errorAFNetworking:error]);
dispatch_group_leave(group);
}];
}
and change your invocation so that it doesn't do dispatch_group_async, but rather just passes the taskGroup parameter:
[[MDAddressManager instance] searchForPlacesContainingText:query
location:alocation
group:taskGroup
completion:^(NSArray* addresses, MDError *error) {
//remoteAddresses is set
});
I have a queue of blocks which perform a webservice call. The problem is that the downloaded data is not freed after the block's end. I read a lot about retains but I can't make ARC dealloc the memory.
Here's the code:
Create the queue of blocks which download the data
- (void)syncData
{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(queue, ^{
[Model syncAziende:^(id response, NSError *error) {
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
[Model syncContatti:^(id response, NSError *error) {
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
[Model syncDestinazioni:^(id response, NSError *error) {
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
and so on...
});
}
On Model.m
+ (void)syncAziende:(RequestFinishBlock)completation
{
__weak typeof(self)selfObject = self;
[selfObject syncData:^(id response, NSError *error) {
completation(response,error);
} wsEndPoint:kCDCEndPointGetAziende tableName:kCDCDBAziendeTableName];
}
+ (void)syncContatti:(RequestFinishBlock)completation
{
__weak typeof(self)selfObject = self;
[selfObject syncData:^(id response, NSError *error) {
completation(response,error);
} wsEndPoint:kCDCEndPointGetContatti tableName:kCDCDBContattiTableName];
}
// and so on...
Where syncData is:
+ (void)syncData:(RequestFinishBlock)completation wsEndPoint:(NSString*) url tableName:(NSString *)table
{
__weak typeof(self)selfObject = self;
[selfObject getDataFromWS:^(id WSresponse, NSError* WSError)
{
completation(nil,nil);
}WSUrl:url];
}
Where getDataFromWS is:
+ (void)getDataFromWS:(RequestFinishBlock)completation WSUrl:(NSString *)svcUrl
{
__weak typeof(self)selfObject = self;
[selfObject getJsonDataFromURL:^(id response, NSError *error)
{
completation(response,error);
}url:svcUrl];
}
Where getJsonDataFromURL is:
+(void)getJsonDataFromURL:(RequestFinishBlock)completation url:(NSString*)url
{
__weak typeof(self)selfObject = self;
__weak AFHTTPRequestOperationManager *manager = [selfObject getAuthorizedRequestionOperationManager];
[manager.requestSerializer setValue:#"application/json" forHTTPHeaderField:#"Accept"];
[manager.requestSerializer setValue:#"application/json" forHTTPHeaderField:#"Content-Type"];
[manager.requestSerializer setValue:#"gzip" forHTTPHeaderField:#"Accept-Encoding"];
[manager GET:url parameters:nil success:^(AFHTTPRequestOperation *operation, __weak id responseObject) {
completation([responseObject objectForKey:#"d"],nil);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
completation(nil,error);
}];
}
These __weak references don't really make sense within the context of a class method. It only makes sense to use the weak-self pattern when dealing with instance methods.
So, this begs the question as to which object you're concerned about not getting released. I don't see anything here that would cause the object that has syncData as an instance method to be retained.
Having said that, this routine with the series of network requests that you have made synchronous through the use of the semaphores would (a) retain any autorelease objects that were instantiated throughout the process (notably, the operations themselves); (b) not allow you to cancel the process once it starts; and (c) run the requests serially (for which you'll pay a serious performance penalty and should be avoided unless you absolutely have to).
I'd suggest a simplification of the code. I'd eliminate the dispatch code with the semaphores. I'd also get rid of all of those weak references to self in class methods.
You should simply issue your GET requests. If you really need them to run serially (as implied by your use of the semaphore), then just set the maxConcurrentOperationCount to 1 for the operationQueue of the AFHTTPRequestOperationManager instance. But don't make them run serially unless you absolutely have to, because you pay a significant performance penalty by doing that. But avail yourself of the NSOperationQueue that AFNetworking provides, rather than also doing your own GCD code (and worse, using semaphores within that GCD code).
But this way, if you want to cancel the requests, you can just call cancelAllOperations for the operationQueue of the AFHTTPRequestOperationManager. You can also control the degree of concurrency with maxConcurrentOperationCount.
Given your description and based on the comments, a naive - but still insufficient -
solution may look as follows:
// A generic completion handler:
typedef void (^completion_t)(id result, NSError* error);
- (void) fetchJSONFromURL:(NSURL*)url completion:(completion_t)completion;
- (void) syncAziendeWithInput:(id)input completion:(completion_t)completion;
- (void) syncContattiWithInput:(id)input completion:(completion_t)completion;
- (void) syncDestinazioniWithInput:(id)input completion:(completion_t)completion;
- (void) syncData
{
dispatch_queue_t sync_queue = dispatch_queue_create("sync_queue", 0); // serial queue
NSURL* url = ...;
[self fetchJSONFromURL:url completion:^(id result, NSError*error){
dispatch_async(sync_queue, ^{
[self syncAziendeWithInput:result completion:^(id result, NSError* error){
...
}];
});
}];
url = ...;
[self fetchJSONFromURL:url completion:^(id result, NSError*error){
dispatch_async(sync_queue, ^{
[self syncContattiWithInput:result completion:^(id result, NSError* error){
...
}];
});
}];
url = ...;
[self fetchJSONFromURL:url completion:^(id result, NSError*error){
dispatch_async(sync_queue, ^{
[self syncDestinazioniWithInput:result completion:^(id result, NSError* error){
...
}];
});
}];
...
}
Caveats:
Oversimplification. A practical solution becomes far more elaborated:
No error handling yet.
No way to cancel the asynchronous tasks.
syncData is asynchronous, but has no completion handler. That is, we don't know when it's complete. We could utilize dispatch_group in order to implement an approach to signal the completion of a number of asynchronous methods.
With the help of a third party library we could implement a solution which does all that above, and looks basically even simpler.
I know there is another similar question, but it's for an older version of AFNetworking, and doesn't really answer it anyway.
I have the following code:
AFHTTPRequestOperationManager* manager = [AFHTTPRequestOperationManager manager];
manager.securityPolicy.allowInvalidCertificates = YES;
manager.requestSerializer = [AFJSONRequestSerializer serializer];
[manager.requestSerializer setAuthorizationHeaderFieldWithUsername: currentUser() password: currentPassword()];
__block NSDictionary* response = nil;
AFHTTPRequestOperation* operation = [manager
GET: #"https://10.20.30.40:8765/foobar"
parameters: [NSDictionary dictionary]
success:^(AFHTTPRequestOperation* operation, id responseObject){
response = responseObject;
NSLog(#"response (block): %#", response);
}
failure:^(AFHTTPRequestOperation* operation, NSError* error){
NSLog(#"Error: %#", error);}
];
[operation waitUntilFinished];
NSLog(#"response: %#", response);
...
If I run this, what I'll see in my log is:
2013-12-09 09:26:20.105 myValve[409:60b] response: (null)
2013-12-09 09:26:20.202 myValve[409:60b] response (block): {
F00005 = "";
F00008 = "";
F00013 = "";
}
The NSLog that is after the waitUntilFinished fires first. I expected it to fire second. What am I missing?
A couple of thoughts:
The issue is that waitUntilFinished will wait for the core network operation to complete, but it will not wait for the success or failure completion blocks. If you want to wait for the completion blocks, you can use a semaphore:
__block NSDictionary* response = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
AFHTTPRequestOperation* operation = [manager GET: #"https://10.20.30.40:8765/foobar"
parameters: [NSDictionary dictionary]
success:^(AFHTTPRequestOperation* operation, id responseObject){
response = responseObject;
NSLog(#"response (block): %#", response);
dispatch_semaphore_signal(semaphore);
}
failure:^(AFHTTPRequestOperation* operation, NSError* error){
NSLog(#"Error: %#", error);
dispatch_semaphore_signal(semaphore);
}];
NSLog(#"waiting");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// [operation waitUntilFinished];
NSLog(#"response: %#", response);
You can, alternatively, wrap this in your own concurrent NSOperation subclass, posting isFinished in the AFHTTPRequestOperation completion blocks, eliminating the semaphore in the process.
Note, make sure to specify completionQueue if doing semaphores on the main queue
because, in the absence of that, AFNetworking defaults to dispatching completion handlers to the main queue and you can deadlock.
As an aside, you should never block the main queue (poor UX, your app could be killed by watchdog process, etc.), so if you're doing this from the main queue, I'd discourage the use of either waitUntilFinished or the semaphore. It's better to just initiate whatever you need from within the completion blocks, letting the main queue continue execution while this asynchronous network operation is in progress, e.g.:
[activityIndicatorView startAnimating];
AFHTTPRequestOperation* operation = [manager GET: #"https://10.20.30.40:8765/foobar"
parameters: [NSDictionary dictionary]
success:^(AFHTTPRequestOperation* operation, id responseObject){
// do whatever you want with `responseObject` here
// now update the UI, e.g.:
[activityIndicatorView stopAnimating];
[self.tableView reloadData];
}
failure:^(AFHTTPRequestOperation* operation, NSError* error){
// put your error handling here
// now update the UI, e.g.:
[activityIndicatorView stopAnimating];
}];
// NSLog(#"waiting");
// dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// // [operation waitUntilFinished];
// NSLog(#"response: %#", response);
It sounds like you want to let your model let the UI do any necessary updates when the model object is done doing its updates. So, you can use your own block parameters so that the view controller can tell the model object what to do when its done (instead of using waitUntilFinished or semaphore to make the network operation block the main queue). For example, let's assume your model had some method like this:
- (void)updateModelWithSuccess:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
AFHTTPRequestOperationManager* manager = [AFHTTPRequestOperationManager manager];
manager.securityPolicy.allowInvalidCertificates = YES;
manager.requestSerializer = [AFJSONRequestSerializer serializer];
[manager.requestSerializer setAuthorizationHeaderFieldWithUsername: currentUser() password: currentPassword()];
AFHTTPRequestOperation* operation = [manager GET: #"https://10.20.30.40:8765/foobar"
parameters: [NSDictionary dictionary]
success:^(AFHTTPRequestOperation* operation, id responseObject){
// do your model update here
// then call the success block passed to this method (if any),
// for example to update the UI
if (success)
success();
}
failure:^(AFHTTPRequestOperation* operation, NSError* error){
NSLog(#"Error: %#", error);
// if caller provided a failure block, call that
if (failure)
failure(error);
}];
}
Then your view controller can do something like:
[modelObject updateModelWithSuccess:^{
// specify UI updates to perform upon success, e.g.
// stop activity indicator view, reload table, etc.
} failure:^(NSError *error){
// specify any UI updates to perform upon failure
}]
Bottom line, your code can use the same style of completion blocks that AFNetworking uses. If you want the model to pass information back, you can add additional parameters to the completion blocks, themselves, but I presume the above illustrates the basic idea.
So I'm rewriting an app for iOS 7 with AFNetworking 2.0 and I'm running into the issue of sending a batch of requests at once and tracking their progress. In the old AFNetworking there was the enqueueBatchOfHTTPRequestOperations:progressBlock:completionBlock: method on AFHTTPClient, this is clearly refactored out and I'm a bit confused on how to enqueue multiple requests.
I have created a subclass of AFHTTPSessionManager and I'm using the POST:... and GET:... methods to communicate with the server. But I can't find anything in the code and/or docs to enqueue multiple requests at once like with the old AFHTTPClient.
The only thing I can find is the undocumented batchOfRequestOperations:progressBlock:completionBlock: method on AFURLConnectionOperation, but that looks like the iOS 6 way of doing this.
Clearly I'm missing something in the new NSURLSession concept that I should use to batch requests or looking over a new AFNetworking feature. Hope someone can help me on the right track here!
tl;dr: How can I send a batch of requests with my AFHTTPSessionManager subclass?
Thanks Sendoa for the link to the GitHub issue where Mattt explains why this functionality is not working anymore. There is a clear reason why this isn't possible with the new NSURLSession structure; Tasks just aren't operations, so the old way of using dependencies or batches of operations won't work.
I've created this solution using a dispatch_group that makes it possible to batch requests using NSURLSession, here is the (pseudo-)code:
// Create a dispatch group
dispatch_group_t group = dispatch_group_create();
for (int i = 0; i < 10; i++) {
// Enter the group for each request we create
dispatch_group_enter(group);
// Fire the request
[self GET:#"endpoint.json"
parameters:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
// Leave the group as soon as the request succeeded
dispatch_group_leave(group);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
// Leave the group as soon as the request failed
dispatch_group_leave(group);
}];
}
// Here we wait for all the requests to finish
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// Do whatever you need to do when all requests are finished
});
I want to look write something that makes this easier to do and discuss with Matt if this is something (when implemented nicely) that could be merged into AFNetworking. In my opinion it would be great to do something like this with the library itself. But I have to check when I have some spare time for that.
Just updating the thread... I had the same problem and after some researches I found some good solutions, but I decided to stick with this one:
I am using the project called Bolts. So, for the same sample above posted by #Mac_Cain13, it would be:
[[BFTask taskWithResult:nil] continueWithBlock:^id(BFTask *task) {
BFTask *task = [BFTask taskWithResult:nil];
for (int i = 0; i < 10; i++) {
task = [task continueWithBlock:^id(BFTask *task) {
return [self executeEndPointAsync];
}];
}
return task;
}] continueWithBlock:^id(BFTask *task) {
// Everything was executed.
return nil;
}];;
- (BFTask *) executeEndPointAsync {
BFTaskCompletionSource *task = [BFTaskCompletionSource taskCompletionSource];
[self GET:#"endpoint.json" parameters:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
[task setResult:responseObject];
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
[task setError:error];
}];
}];
return task.task;
}
Basically, it's stacking all of the tasks, waiting and unwrapping until there is no more tasks, and after everything is completed the last completion block is executed.
Another project that does the same thing is RXPromise, but for me the code in Bolts was more clear.
For request which can be post or get, you can use AFNetworking 2.0 for batch operation as firstly you need to create operation like this:
//Request 1
NSString *strURL = [NSString stringWithFormat:#"your url here"];
NSLog(#"scheduleurl : %#",strURL);
NSDictionary *dictParameters = your parameters here
NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] requestWithMethod:#"POST" URLString:strURL parameters:dictParameters error: nil];
AFHTTPRequestOperation *operationOne = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operationOne = [AFHTTPResponseSerializer serializer];
[operationOne setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
//do something on completion
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"%#",[error description]);
}];
//Request 2
NSString *strURL1 = [NSString stringWithFormat:#"your url here"];
NSLog(#"scheduleurl : %#",strURL);
NSDictionary *dictParameters1 = your parameters here
NSMutableURLRequest *request1 = [[AFHTTPRequestSerializer serializer] requestWithMethod:#"POST" URLString:strURL1 parameters:dictParameters1 error: nil];
AFHTTPRequestOperation *operationTwo = [[AFHTTPRequestOperation alloc] initWithRequest:request1];
operationTwo = [AFHTTPResponseSerializer serializer];
[operationTwo setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
//do something on completion
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"%#",[error description]);
}];
//Request more here if any
Now perform batch operation like this :
//Batch operation
//Add all operation here
NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:#[operationOne,operationTwo] progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations)
{
NSLog(#"%i of %i complete",numberOfFinishedOperations,totalNumberOfOperations);
//set progress here
yourProgressView.progress = (float)numberOfFinishedOperations/(float)totalNumberOfOperations;
} completionBlock:^(NSArray *operations)
{
NSLog(#"All operations in batch complete");
}];
[[NSOperationQueue mainQueue] addOperations:operations waitUntilFinished:NO];
On AFNetworking 2.0, AFHTTPClient has been split on AFHTTPRequestOperationManager and AFHTTPSessionManager, so probably you could start with the first, which has operationQueue property.
Currently, NSURLSession tasks are not suitable for the same kind of patterns request operations use. See the answer from Mattt Thompson here regarding this issue.
Direct answer: if you need dependencies or batches, you'll still need to use request operations.
I am doing a lot of URL requests (about 60 small images) and I have started to do them Asynchronously. My code adds another image (little downloading thing) and then sets a Request going.
When the request is done I want "data" to be put in the location which was originally added for it, however, I can not see how to pass "imageLocation" to the block for it to store the image in the correct location.
I have replaced the 3rd line with below which seems to work but I am not 100% it is correct (it is very hard to tell as the images are nearly identical). I am also thinking that it is possible to pass "imageLocation" at the point where the block is declared.
Can any confirm any of this?
__block int imageLocation = [allImages count] - 1;
// Add another image to MArray
[allImages addObject:[UIImage imageNamed:#"downloading.png"]];
imageLocation = [allImages count] - 1;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setTimeoutInterval: 10.0];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue currentQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data != nil && error == nil)
{
//All Worked
[allImages replaceObjectAtIndex:imageLocation withObject:[UIImage imageWithData:data]];
}
else
{
// There was an error, alert the user
[allImages replaceObjectAtIndex:imageLocation withObject:[UIImage imageNamed:#"error.png"]];
}];
Dealing with asynchronous methods is a pain ;)
In your case its guaranteed that the completion block will execute on the specified queue. However, you need to ensure that the queue has a max concurrent operations count of 1, otherwise concurrent access to shared resources is not safe. That's a classic race http://en.wikipedia.org/wiki/Race_condition. The max concurrent operations of a NSOperationQueue can be set with a property.
In general, completion handlers may execute on any thread, unless otherwise specified.
Dealing with asynchronous methods gets a lot easier when using a concept called "Promises" http://en.wikipedia.org/wiki/Promise_(programming). Basically, "Promises" represent a result that will be evaluated in the future - nonetheless the promise itself is immediately available. Similar concepts are named "futures" or "deferred".
There is an implementation of a promise in Objective-C on GitHub: RXPromise. When using it you also get safe access from within the handler blocks to shared resources. An implementation would look as follows:
-(RXPromise*) fetchImageFromURL:(NSString*)urlString queue:(NSOperationQueue*) queue
{
#autoreleasepool {
RXPromise* promise = [[RXPromise alloc] init];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data != nil) {
[promise fulfillWithValue:data];
}
else { // There was an error
[promise rejectWithReason:error];
};
}];
return promise;
}
}
Then call it:
- (void) fetchImages {
...
for (NSUInteger index = 0; index < N; ++index)
{
NSString* urlString = ...
[self fetchImageFromURL:urlString, self.queue]
.then(^id(id data){
[self.allImages replaceObjectAtIndex:index withObject:[UIImage imageWithData:data]];
return #"OK";
},
^id(NSError* error) {
[self.allImages replaceObjectAtIndex:index withObject:[UIImage imageNamed:#"error.png"]];
return error;
});
}
}
A couple of thoughts:
If you want to download 60 images, I would not advise using a serial queue for the download (e.g. do not use an operation queue with maxConcurrentOperationCount of 1), but rather use a concurrent queue. You will want to synchronize the updates to make sure your code is thread-safe (and this is easily done by dispatching the updates to a serial queue, such as the main queue, for that final update of the model), but I wouldn't suggest using a serial queue for the download itself, as that will be much slower.
If you want to use the NSURLConnection convenience methods, I'd suggest something like the following concurrent operation request approach (where, because it's on a background queue, I'm using sendSynchronousRequest rather than sendAsynchronousRequest), where I'll assume you have an NSArray, imageURLs, of NSURL objects for the URLs of your images:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"all done %.1f", CFAbsoluteTimeGetCurrent() - start);
NSLog(#"allImages=%#", self.allImages);
}];
[imageURLs enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] returningResponse:&response error:&error];
if (!data) {
NSLog(#"%s sendSynchronousRequest error: %#", __FUNCTION__, error);
} else {
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self.allImages replaceObjectAtIndex:idx withObject:image];
});
}
}
}];
[queue addOperation:operation];
[completionOperation addDependency:operation];
}];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
A couple of asides: First, I'm using an operation queue rather than a GCD concurrent queue, because it's important to be able to constrain the degree on concurrency. Second, I've added a completion operation, because I assume it would be useful to know when all the downloads are done, but if you don't need that, the code is obviously simper. Third, that benchmarking code using CFAbsoluteTime is unnecessary, but useful solely for diagnostic purposes if you want to compare the performance using a maxConcurrentOperationCount of 4 versus 1.
Better than using the NSURLConnection convenience methods, above, you might want to use a NSOperation-based network request. You can write your own, or better, use a proven solution, like AFNetworking. That might look like:
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"all done %.1f", CFAbsoluteTimeGetCurrent() - start);
NSLog(#"allImages=%#", self.allImages);
}];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFImageResponseSerializer serializer];
manager.operationQueue.maxConcurrentOperationCount = 4;
[imageURLs enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
NSOperation *operation = [manager GET:[url absoluteString] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self.allImages replaceObjectAtIndex:idx withObject:responseObject];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%s image request error: %#", __FUNCTION__, error);
}];
[completionOperation addDependency:operation];
}];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
Because AFNetworking dispatches those completion blocks back to the main queue, that solves the synchronization issues, while still enjoying the concurrent network requests.
But the main take-home message here is that an NSOperation-based network request (or at least one that uses NSURLConnectionDataDelegatemethods) opens additional opportunities (e.g. you can cancel all of those network requests if you have to, you can get progress updates, etc.).
Frankly, having walked through two solutions that illustrate how to download the images efficiently up-front, I feel compelled to point out that this is an inherently inefficient process. I might suggest a "lazy" image loading process, that requests the images asynchronously as they're needed, in a just-in-time (a.k.a. "lazy") manner. The easiest solution for this is to use a UIImageView category, such as provided by AFNetworking or SDWebImage. (I'd use AFNetworking's if you're using AFNetworking already for other purposes, but I think that SDWebImage's UIImageView category is a little stronger.) These not only seamlessly load the images asynchronously, but offer a host of other advantages such as cacheing, more efficient memory usage, etc. And, it's as simple as:
[imageView setImageWithURL:url placeholder:[UIImage imageNamed:#"placeholder"]];
Just a few thoughts on efficiently performing network requests. I hope that helps.