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?
Related
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 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.
I just recently switched to AFNetworking to handle all my networking within my app. However, it now appears to be blocking the main thread so my MBProgressHUD won't spin until after the operation finishes and my pullToRefreshView will also not animate until after the operation. How would I fix this?
- (void)pullToRefreshViewShouldRefresh:(PullToRefreshView *)view; {
// Call the refreshData method to update the table
[dataController refreshData];
}
- (void)refreshData {
NSURLRequest *request = [NSURLRequest requestWithURL:[FCDataController parserURL]];
NSLog(#"URL = %#", request);
AFXMLRequestOperation *operation = [AFXMLRequestOperation XMLParserRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSXMLParser *XMLParser) {
_calls = [[NSMutableArray alloc] init];
XMLParser.delegate = self;
[XMLParser parse];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, NSXMLParser *XMLParser) {
if ([delegate respondsToSelector:#selector(refreshDataDidFailWithError:)]) {
[delegate refreshDataDidFailWithError:error];
}
}];
[operation start];
}
By default, AFNetworking calls the success/failure blocks on the main thread (after the network operation runs on a background thread). This is a convenience for the common case where your code just needs to update the UI. If you need to do some more complex operation with the results (like parsing a big XML document), then you can specify some other dispatch queue on which your callback should be run. See the documentation for more.
Update (11 Feb 2016): AFNetworking has changed quite a bit in the nearly three years since I posted this answer: AFHTTPRequestOperation doesn't exist any more in the current version (3.0.4). I've updated the link so it's not broken, but the way you'd accomplish something similar these days is likely quite different.
Where is the MBProgressHUD being called? Are you using SSPullToRefresh or some other implementation. I'm writing very similar code on a current project and its working great.
- (BOOL)pullToRefreshViewShouldStartLoading:(SSPullToRefreshView *)view {
return YES;
}
- (void)pullToRefreshViewDidStartLoading:(SSPullToRefreshView *)view {
[self refresh];
}
- (void)refresh {
NSURL* url = [NSURL URLWithString:#"some_url_here"];
NSURLRequest* request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation* operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
// consume response
[_pullToRefreshView finishLoading];
[self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
}];
[operation start];
My guess is that - (void)pullToRefreshViewShouldRefresh:(PullToRefreshView *)view; { is being called from a background thread.
I'm new to using blocks in iOS and I am thinking that's probably the crux of my problem.
I just want to build a simple static DataManager class whose sole job is to fetch data from my Restful service.
I would call this from all my various UIViewControllers (or collectionview/table controllers)
In my class i have a function that looks like this
+ (NSArray *) SearchByKeyword: (NSString*) keyword {
__block NSArray* searchResults = [[NSArray alloc] init];
NSString *baseURL = #"http://someURL.com/api/search";
NSString *requestURL = [baseURL stringByAppendingString:keyword];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:baseURL]];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET"
path:requestURL
parameters:nil];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
searchResults = [JSON valueForKeyPath:#""];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
[operation start];
return searchResults;
}
However, this keeps returning zero data. Can someone suggest the right way of doing this?
You are trying to use the results of an asynchronous task (the JSON operation) as the return value for a synchronous method call, so that is why you get no data.
You could provide your view controllers with an API that takes completion blocks and failure blocks, similar to the AF networking one. View controllers can then do what they need to do with the results when they are passed into the block.
Modifying your code from your question:
typedef void (^SearchCompletionBlock)(NSArray *results);
typedef void (^SearchFailureBlock)(NSError *error);
+ (void)searchByKeyword:(NSString*)keyword completionBlock:(SearchCompletionBlock)completionBlock failureBlock:(SearchFailureBlock)failureBlock;
{
NSString *baseURL = #"http://someURL.com/api/search";
NSString *requestURL = [baseURL stringByAppendingString:keyword];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:baseURL]];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET"
path:requestURL
parameters:nil];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
if (completionBlock) {
completionBlockc([JSON valueForKeyPath:#""]);
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
if (failureBlock) {
failureBlock(error);
}
}];
[operation start];
}
Then clients could pass completion blocks that stored the results and reloaded their views. Something like:
^ (NSArray *results) {
self.results = results;
[self.tableView reloadData];
}
Your JSON request operation is asynchronous, meaning that it will kick off the request ([operations start], then immediately return your results, which will be empty. When the completion block runs, it assigns your data but nothing is done with it. Your search method can't return an object unless it waits for the request to complete.
You've got a few options:
Pass in a completion block to the search method which does something with the results. The completion block is called in the completion block of the request, once all the service-specific stuff (processing JSON etc) is finished. (Block inception!)
Have the completion block of the request assign a property of the data manager, then call a delegate method or notification to let others know the results are available.
I'd prefer option 1.
I'm trying to use the AFNetworking UIImageView call to load images from a URL as shown below:
[self.image setImageWithURL:[NSURL URLWithString:feed.imageURL] placeholderImage: [UIImage imageNamed:#"logo"]];
The placeholder image always shows up, but the actual image from "feed.imageURL" never does. I've verified that the URL is actually correct. I even hardcoded it to make sure, and still nothing.
My basic app setup is a tab controller...and in viewDidLoad, I call a method "fetchFeed" which performs the HTTP request to gather my JSON data.
My request block looks like:
AFJSONRequestOperation *operation = [AFJSONRequestOperation
JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
[self parseDictionary:JSON];
isLoading = NO;
[self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Error: %#", error);
[self showNetworkError];
isLoading = NO;
[self.tableView reloadData];
}];
operation.acceptableContentTypes = [NSSet setWithObjects:#"application/json", #"text/json", #"text/javascript", #"text/html", nil];
[queue addOperation:operation];
Turns out the server I was requesting the image from was sending content-type "image/jpg" and by default AFNetworking does not support this file type.
I changed the class method in AFImageRequestOperation to look like:
+ (NSSet *)defaultAcceptableContentTypes {
return [NSSet setWithObjects:#"image/tiff", #"image/jpeg", #"image/gif", #"image/png", #"image/ico", #"image/x-icon" #"image/bmp", #"image/x-bmp", #"image/x-xbitmap", #"image/x-win-bitmap", #"image/jpg", nil];
}
and it fixed my problem.
You can manage to accept what content-type you want with this library simply changing the request like this:
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:yourURL];
[request addValue:#"image/*" forHTTPHeaderField:#"Accept"];
And call the AFNetworking method:
AFJSONRequestOperation *operation = [AFJSONRequestOperation
JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
}];
This way you will be able to override the content-type without changing the library.
AFNetworking doesn't support image/jpg MIME TYPE by default.
You can support it without modifying the AFNetworking Library
[AFImageRequestOperation addAcceptableContentType:#"image/jpg"];
All operations that manipulate the UI must be performed on the main thread. So you may need to use 'performSelectorOnMainThread:' when reloading your tableview data in the completion block.
[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO]
I had a similar problem but it turned out that I was passing a URL which contained spaces in it. When I properly encoded the URL using stringByAddingPercentEscapesUsingEncoding: the images now load.