I want to execute a series of AFJSONRequestOperation in order, and be able to interrupt the queue when one fails.
At the moment, the way I do it is not reliable, as sometimes the next operation will get a chance to start.
I have a singleton to call my api endpoint
AFJSONRequestOperation *lastOperation; // Used to add dependency
NSMutableArray *operations = [NSMutableArray array]; // Operations stack
AFAPIClient *httpClient = [AFAPIClient sharedClient];
[[httpClient operationQueue] setMaxConcurrentOperationCount:1]; // One by one
And then I add the operations this way
NSMutableURLRequest *request = ...; // define request
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
// Takes care of success
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
[[httpClient operationQueue] setSuspended:YES];
[[httpClient operationQueue] cancelAllOperations];
}];
[push:operation addDependency:lastOperation];
[operations $push:operation]; // This is using ConciseKit
lastOperation = operation;
// Repeat with other operations
// Enqueue a batch of operations
[httpClient enqueueBatchOfHTTPRequestOperations:operations ...
Trouble is, sometimes the operation following the one that fails still gets a chance to start.
So it seems that that having 1 concurrent operation max and a dependency chain isn't enough to tell the queue to wait until after the failure callback is fully executed.
What's the proper way to do this ?
Thanks
The failure callback is executed on the main thread and the operation (which is running on a background thread) doesn't wait for it. So, you'd need to do some editing to prevent the next operation from starting before the operation and its completion blocks have completed.
Or, instead of putting all the operations into the queue at the start, hold the list of operations in an array and just add the next operation following each success.
Related
I want to display an image on the screen which I take from the internet. I have used
NSURLConnection to create an asynchronous call to take the data and, in the response block, I called the code to assign it to an UIImage object.
My question is why do I need to call sleep(1) after the block execution? If i'm not calling it, then my image is not drawn on the screen. Is it another, more elegant way to achive this?
-(void)loadImage:(NSString *)url
{
NSURL *imageURL = [NSURL URLWithString:url];
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageURL cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:5.0f];
[NSURLConnection sendAsynchronousRequest:imageRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if(!connectionError) {
if(data) {
//there goes the main thingy
self.myView.wallpaperImage = [UIImage imageWithData:data];
[self.myView setNeedsDisplay];
} else {
NSLog(#"No data found at url:%#",url);
}
} else {
NSLog(#"Could not connect to %#",url);
}
}];
sleep(1);
}
This:
self.myView.wallpaperImage = [UIImage imageWithData:data];
[self.myView setNeedsDisplay];
Is happening on the thread managed by the NSOperationQueue passed to sendAsynchronousRequest. Those methods need to be called from the main thread.
Your sleep may be causing the main thread's runloop to iterate, after which those calls appear to have worked.
To fix this, and to avoid a whole bunch of other problems your current approach will have, do this:
[NSURLConnection sendAsynchronousRequest:imageRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if([data length] > 0) {
//there goes the main thingy
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.myView.wallpaperImage = [UIImage imageWithData:data];
[self.myView setNeedsDisplay];
}];
} else {
// Perform your error handling here.
}
}];
This will use [NSOperationQueue mainQueue] to perform those UIKit calls from the main queue - not libdispatch. libdispatch is a low level interface, it is a recommended best practice to always prefer the higher level interface - in this case, NSOperationQueue. UIKit is only safe when called from the main thread (or queue).
It also changes your error handling behavior to follow the best practices for the platform - check the result of your call (in this case, data) and THEN process any error returned.
Your code is actually a good example of why blocks retain captured objects (in this case self). If there was no retain cycle here, ARC could destroy queue as soon as it goes out of scope, and the block would never execute. Instead, because of the retain cycle, the queue stays around until the block has executed.
I'm setting in a ViewController a NSString property by fetching a JSON table and then in a different ViewController I want to get that same property.
What is happening is when I'm trying to get the property this is nil.
I know what is the problem, I'm accessing the property in the main thread while the JSON fetching is still in progress in another thread.
I'm using the AFNETWORKING 2.0 framework to access the JSON table.
How can I wait for the property set and then use it?
I really appreciate any help you can provide.
You can do this in different ways, you can post notification from AFnetworkingJSON operation success callback like this. And observer that notification where you want to access that property. You can also pass a completionHandler to the method which can be call from success or failure callbacks.
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"link"]];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
// Post notification from here
// call completion handler if you have any
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response,
}
];
[operation start];
I wrote a POST method that needs to return a jSON to the viewcontroller that called her.
If i add in the success block return _jsonDictionary; I will get this error:
Incompatible block pointer types sending 'id (^)(NSURLRequest *__strong, NSHTTPURLResponse *__strong, __strong id)' to parameter of type 'void (^)(NSURLRequest *__strong, NSHTTPURLResponse *__strong, __strong id)'
I guessing that because it's asynchronous, adding a return will force it to be synchronous but, I want all my POST methods for my app to be in one class so getting the data out of the JSON to variables that are declared across my app using something like valueForKey makes things a bit complicated for me.
Is that bad design?
-(NSDictionary *)getData
{
_jsonDictionary = [[NSDictionary alloc]init];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"%#/getSomething",MainURL ]];
[AFJSONRequestOperation addAcceptableContentTypes:[NSSet setWithObject:#"text/html"]];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"POST" path:nil parameters:nil];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
_jsonDictionary = JSON;
NSLog(#"jsonDictionary: %#",_jsonDictionary);
}
failure:^(NSURLRequest *request , NSURLResponse *response , NSError *error , id JSON)
{
NSLog(#"request: %#",request);
NSLog(#"Failed: %#",[error localizedDescription]);
}];
[httpClient enqueueHTTPRequestOperation:operation];
}
Another question, why do I get this warning at the end of the above code: Control reaches end of non-void function even if i change the name of the method in .m and in .h to -(void )getData ??
If your main concern is to "get back" your data, you have three ways (maybe more, but I can only things of those three):
in your getData method, you can post a NSNotification that your viewController subscribed to before calling getData
if you are using (or plan to use) a dataManager as a singleton, your viewController can KVO on the #property of the dataManager
my favorite: in your calling viewController, you construct and pass a block to your getData method that will be called (with or w/o the result). This is exactly what you are doing when building the AFJSONRequestOperation in your example.
The error you are getting is correct. The block is not supposed to return anything, and your return _jsonDictionary; is trying just that.
What you need to do is update _jsonDictionary inside the success block (like you already do), and then invoke another function to fire up events that refresh you UI (something like calling [self.tableView refreshData]).
you're totally mixing asyn and sync ... there IS valid no _jsonDictionary that getData could return. the _jsonDictionary is only filled asynchronously when the completion blocks are called.
you need to proceed from there.... call another method for example
as for the error/warning you see ... getData is supposed to return something (NSDictionary*) or a void* (almost eqaul to an id)
to not return stuff, it is void only.
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.
In my app I have a series of background tasks to be performed one after the other (say tasks : A, B and C). Each of these tasks talk to different web services (XML). I am using AFXMLRequestOperation of AFNetworking library to initiate request to the web service and handling the parsing logic at the success block.
Each following task is dependent on the successful completion of the previous task. Also, I want the following task to be called after a delay of few seconds after the successful completion of previous. Once task C completes successfully, I'm done.
All of this is happening in the background thread, and hence UI thread is always responsive throughout (my UIActivityIndicator keeps moving throughout for each tasks separately).
Here's the pseudo code snippet:
-(void)viewDidLoad
{
_operationQueue = [[NSOperationQueue alloc]init];
[_operationQueue setMaxConcurrentOperationCount:1];
[self taskA];
}
- taskA
{
NSMutableURLRequest *request = urlA;
AFXMLRequestOperation *operationA = [AFXMLRequestOperation XMLParserRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSXMLParser *XMLParser) {
XMLParser.delegate = someClass;
[XMLParser parse];
// Now since the operation is successful, start task B after a delay of 5 seconds
[self performSelector:#selector(taskB) withObject:nil afterDelay:5];
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, NSXMLParser *XMLParser) {
NSLog(#"NSError: %#",error);
}];
[_operationQueue addOperation: operationA];
}
- taskB
{
NSMutableURLRequest *request = urlB;
AFXMLRequestOperation *operationB = [AFXMLRequestOperation XMLParserRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSXMLParser *XMLParser) {
XMLParser.delegate = someOtherClass;
[XMLParser parse];
// Now since the operation is successful, start task C after 10 seconds
[self performSelector:#selector(taskC) withObject:nil afterDelay:10];
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, NSXMLParser *XMLParser) {
}];
[_operationQueue addOperation: operationB];
[operationB addDependency:operationA]; // This code seems to produce no result and hence seems redundant
}
- taskC
{
NSMutableURLRequest *request = urlB;
AFXMLRequestOperation *operationC = [AFXMLRequestOperation XMLParserRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSXMLParser *XMLParser) {
XMLParser.delegate = yetAnotherClass;
[XMLParser parse];
// Now since the operation is successful, mission accomplished!
NSLog(#"Mission accomplished!");
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, NSXMLParser *XMLParser) {
}];
[_operationQueue addOperation: operationB];
[operationC addDependency:operationB]; // This code seems to produce no result and hence seems redundant
}
Questions:
I'm able to achieve what I wanted to from the app with this implementation, but I'm not sure if I'm making the right use of NSOperation and NSOperationQueue. Of what I read from Apple docs and tutorials, one of the strengths of NSOperation is using it for dependency establishment between different operations. However, in my example how can I ensure operationB gets executed only after the 'successful' completion of task A and thus leverage the "addDependency" feature of NSOperation?
I also want to ensure that 'taskB' gets called only after a certain
delay after successful completion of 'taskA' and so on. Is [self performSelector:#selector(taskB) withObject:nil afterDelay:5];
the only way to do it? Or are there alternative ways, where I could
use some elements of NSOperation/NSOperationQueues? Or maybe use
something like "dispatch_after"??
Overall, how can I redesign the code better to get the same tasks accomplished using NSOperation?