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.
Related
I have the following code inside a class (static method) which I call to get data from an API. I decided to make this a static method just so I can reuse it on some other parts of the app.
+ (NSArray*) getAllRoomsWithEventId:(NSNumber *)eventId{
NSURL *urlRequest = [NSURL URLWithString:[NSString stringWithFormat:#"http://blablba.com/api/Rooms/GetAll/e/%#/r?%#", eventId, [ServiceRequest getAuth]]];
NSMutableArray *rooms = [[NSMutableArray alloc] init];
NSURLRequest *request = [NSURLRequest requestWithURL:urlRequest];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSLog(#"Response of getall rooms %#", JSON);
NSArray *jsonResults = (NSArray*)JSON;
for(id item in jsonResults){
Room* room = [[Room alloc]init];
if([item isKindOfClass:[NSDictionary class]]){
room.Id = [item objectForKey:#"Id"];
room.eventId = [item objectForKey:#"EventId"];
room.UINumber = [item objectForKey:#"RoomUIID"];
[rooms addObject:room];
}
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON){
NSLog(#"Error");
}];
[operation start];
[operation waitUntilFinished];
return rooms;
}
Now my issue is, whenever I call this in a ViewController (ViewDidLoad method). The static method will run till the end and will return null on the rooms, but the Nslog will display the "Success" block Nslog a few seconds after. Now I understand that this is asynchronous so it doesn't wait for the success block to execute before it reaches the "return rooms;" line. With all that said, I need some advice as to how to handle this, like maybe a progress bar or something like that? Or something that delays it? I'm not really sure if that's the reight way or if it is, I am not sure how to do it.
Any advice is very much appreciated. Thank you!
AFNetworking is built around asynchronicity—starting a request, and then executing some piece of code once that request has finished.
waitUntilFinished is an anti-pattern, which can block the user interface.
Instead, your method should have no return type (void), and have a completion block parameter that returns the serialized array of rooms:
- (void)allRoomsWithEventId:(NSNumber *)eventId
block:(void (^)(NSArray *rooms))block
{
// ...
}
See the example app in the AFNetworking project for an example of how to do this.
You can write your method following way:
+ (void) getAllRoomsWithEventId:(NSNumber *)eventId:(void(^)(NSArray *roomArray)) block
{
NSURL *urlRequest = [NSURL URLWithString:[NSString stringWithFormat:#"http://blablba.com/api/Rooms/GetAll/e/%#/r?%#", eventId, [ServiceRequest getAuth]]];
NSMutableArray *rooms = [[NSMutableArray alloc] init];
NSURLRequest *request = [NSURLRequest requestWithURL:urlRequest];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSLog(#"Response of getall rooms %#", JSON);
NSArray *jsonResults = (NSArray*)JSON;
for(id item in jsonResults){
Room* room = [[Room alloc]init];
if([item isKindOfClass:[NSDictionary class]]){
room.Id = [item objectForKey:#"Id"];
room.eventId = [item objectForKey:#"EventId"];
room.UINumber = [item objectForKey:#"RoomUIID"];
[rooms addObject:room];
}
}
block(rooms);
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON){
NSLog(#"Error");
block(nil); //or any other error message..
}];
[operation start];
[operation waitUntilFinished];
}
you can call this method like followings:
[MyDataClass getAllRoomsWithEventId:#"eventid1":^(NSArray *roomArray) {
NSLog(#"roomArr == %#",roomArray);
}];
In the following code snippet I am authenticating with a web service and retrieving very tiny photos (filesize < 50kb) into core data. This works relatively ok but randomly it doesn't return any photo.
I have checked the URLs that I am passing to it for each photo are correct and resolve correctly. I need it to return a photo everytime.
LogInfo("IMPORTING PHOTO BINARY DATA.");
NSString *photoURL = value;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:photoURL]];
AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request imageProcessingBlock:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
LogInfo(#"RETRIEVED PHOTO IN setValue.");
NSData* imageData = [NSData dataWithData:UIImagePNGRepresentation(image)];
LogInfo(#"PUTTING THE IMAGE INTO CORE DATA IN setValue.");
[managedObject setValue:imageData forKey:#"personphoto"];
}failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
LogInfo(#"ERROR GETTING PHOTO IN setValue.");
}];
[operation setAuthenticationChallengeBlock:^(NSURLConnection *connection, NSURLAuthenticationChallenge *challenge) {
NSURLCredential *newCredential = [NSURLCredential credentialWithUser:self.strSPUser password:self.strSPPW persistence:NSURLCredentialPersistenceForSession];
[challenge.sender useCredential:newCredential forAuthenticationChallenge:challenge];
}];
NSOperationQueue* operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperation:operation];
[managedObject setValue:value forKey:#"personimageurl"];
The "personimageurl" gets set correctly everytime but the "personphoto" in core data does not get updated with the image data every time.
[EDIT]
After some further testing I have noticed that when I have a higher bandwidth connection that more of the images are downloaded and stored in core data than when I have a slower connection. This would point to something being wrong with AFImageRequestOperation performance.
[EDIT
I've changed my code around to work as follows:
IN MYHTTPCLIENT.M :
- (void)downloadImageWithCompletionBlock:(void (^)(UIImage *downloadedImage))completionBlock identifier:(NSString *)identifier {
NSString* urlString = identifier;
AFImageRequestOperation* operation = [AFImageRequestOperation imageRequestOperationWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]] imageProcessingBlock:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
LogInfo(#"SUCCESS GETTING PHOTO: %#", response);
completionBlock(image);
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
LogInfo(#"ERROR GETTING PHOTO IN downloadImageWithCompletionBlock.");
}];
[operation setAuthenticationChallengeBlock:^(NSURLConnection *connection, NSURLAuthenticationChallenge *challenge) {
NSURLCredential *newCredential = [NSURLCredential credentialWithUser:self.strSPUser password:self.strSPPW persistence:NSURLCredentialPersistenceForSession];
[challenge.sender useCredential:newCredential forAuthenticationChallenge:challenge];
}];
[self enqueueHTTPRequestOperation:operation];
}
Then I call the above method in MYSYNCENGINE.M as follows:
[[OLHTTPClient sharedClient] downloadImageWithCompletionBlock:^(UIImage *downloadedImage) {
LogInfo(#"RETRIEVED PHOTO IN setValue.");
NSData* imageData = [NSData dataWithData:UIImagePNGRepresentation(downloadedImage)];
[managedObject setValue:value forKey:#"olpersonimageurl"];
LogInfo(#"PUTTING THE IMAGE INTO CORE DATA IN setValue.");
[managedObject setValue:imageData forKey:#"olpersonphoto"];
} identifier:photoURL];
Now when I am connected on a high bandwidth connection I get most of the images returned but they are never returned the first time I call the above method. With every subsequent call the images are returned.
No errors are received at all and debugging shows that every image is returned successfully but not all of the images end up in core data.
Any ideas why this is happening?
[END EDIT]
Easy solution: don't store image data in Core Data.
NSURLCache provides in-memory as well as disk-based storage, and respects HTTP cacheing rules. Storing image URLs for each managed object and loading images on-demand is a likely a much better way to go.
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?
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.