Photos Being Returned Randomly From AFImageRequestOperation - ios

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.

Related

How to tell if blocks in loop all have completed executing?

I have a loop set up that downloads a series a images which I will later use for to animate using the animationImages property of UIImageView. I would like to know when all the blocks inside my loops have finished executing so I could begin the animation, and was wondering how I may be able to tell when they are finished completing? Thanks!
for (PFObject *pictureObject in objects){
PFFile *imageFile = [pictureObject objectForKey:#"image"];
NSURL *imageFileURL = [[NSURL alloc] initWithString:imageFile.url];
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageFileURL];
[tokenImageView setImageWithURLRequest:imageRequest placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
[self.downloadedUIImages addObject:image]; //This is a mutableArray that will later be set to an UIImageView's animnationImages
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"Error %#", error);
}];
}
//When I know all the blocks have finished downloading, I will then to animate the downloaded images.
Edit: having issue with Error -999
I am encountering the following issue when executing the code in the provided answer: Domain=NSURLErrorDomain Code=-999 "The operation couldn’t be completed. (NSURLErrorDomain error -999.)"
A quick search reveals that Error -999 means "another request is made before the previous request is completed" ... which is certainly the case here since I am making several requests in quick succession. The recommended fix suggested here didn't work for me as it will only successfully download one UIImage (the last one requested) , with the previous ones failing. I was wondering if there is workaround here or in AFNetworking that I ought to consider? Thanks!
Edit 2: working code based on #David's solution
for (PFObject *pictureObject in objects){
PFFile *imageFile = [pictureObject objectForKey:#"image"];
NSURL *imageFileURL = [[NSURL alloc] initWithString:imageFile.url];
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageFileURL];
AFHTTPRequestOperation *requestOperation = [[AFHTTPRequestOperation alloc] initWithRequest:imageRequest];
requestOperation.responseSerializer = [AFImageResponseSerializer serializer];
dispatch_group_enter(group);
[requestOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Response: %#", responseObject);
UIImage *retrivedImage = (UIImage *)responseObject;
[self.downloadedUIImages addObject:retrivedImage];
dispatch_group_leave(group);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Image error: %#", error);
dispatch_group_leave(group);
}];
[requestOperation start];
counter ++;
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(#"Horray everything has completed");
NSLog(#"What is here %#", self.downloadedUIImages);
NSLog(#"Done");
});
Create a dispatch group, in the for loop enter the group, in the completion block leave the group. Then you can use dispatch_group_notify to find out when all blocks have completed:
dispatch_group_t group = dispatch_group_create();
for (PFObject *pictureObject in objects){
PFFile *imageFile = [pictureObject objectForKey:#"image"];
NSURL *imageFileURL = [[NSURL alloc] initWithString:imageFile.url];
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageFileURL];
dispatch_group_enter(group);
[tokenImageView setImageWithURLRequest:imageRequest placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
[self.downloadedUIImages addObject:image]; //This is a mutableArray that will later be set to an UIImageView's animnationImages
dispatch_group_leave(group);
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"Error %#", error);
dispatch_group_leave(group);
}];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// do your completion stuff here
});
Count how many you've completed. The challenging part is making it thread safe. I recommend creating an atomic counter class for that.
Generic solution!
+ (void)runBlocksInParallel:(NSArray *)blocks completion:(CompletionBlock)completion {
AtomicCounter *completionCounter = [[AtomicCounter alloc] initWithValue:blocks.count];
for (AsyncBlock block in blocks) {
block(^{
if ([completionCounter decrementAndGet] == 0) {
if (completion) completion();
}
});
}
if (blocks.count == 0) {
if (completion) completion();
}
}
NSMutableArray *asyncBlocks = [NSMutableArray array];
for (PFObject *pictureObject in objects){
[asyncBlocks addObject:^(CompletionBlock completion) {
PFFile *imageFile = [pictureObject objectForKey:#"image"];
NSURL *imageFileURL = [[NSURL alloc] initWithString:imageFile.url];
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageFileURL];
[tokenImageView setImageWithURLRequest:imageRequest placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
[self.downloadedUIImages addObject:image]; //This is a mutableArray that will later be set to an UIImageView's animnationImages
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"Error %#", error);
} completion:completion];
}];
}
[BlockRunner runBlocksInParallel:[asyncBlocks copy] completion:^{
//Do your final completion here!
}];
Set up a property and initialize it to the number of cycles - objects.count. In the completion of the block, lower the number down. When you reach zero, you are done.
for (PFObject *pictureObject in objects){
PFFile *imageFile = [pictureObject objectForKey:#"image"];
NSURL *imageFileURL = [[NSURL alloc] initWithString:imageFile.url];
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:imageFileURL];
[tokenImageView setImageWithURLRequest:imageRequest placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
[self.downloadedUIImages addObject:image]; //This is a mutableArray that will later be set to an UIImageView's animnationImages
if([[objects lastObject] isEqual:pictureObject]) {
[self animateImages];
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"Error %#", error);
if([[objects lastObject] isEqual:pictureObject]) {
[self animateImages];
}
}];
}
- (void)animateImages {
//do animation here.
}

AFNetworking : Handling of asynchronous get request with AFJSONRequestOperation

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);
}];

iOS AFNetworking blocking main thread

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.

Possible to add property to object in array in blockopertion (AFNetwork)?

I'm making several request from different sources, and because of this I want to add a property like: '"newsSource" = twitter' (JSON format) to the created NSArray resultsTwitter below. The reason is I want be able to handle each "newsitem" uniquely.
I'm new to blocks, but I think it might be an really easy way to do this "on the fly"?
If not possible within the block operation, any suggestion on how to do it after operation is done?
// Fetch data from Twitter (json complient)
NSURLRequest *request = [NSURLRequest requestWithURL:urlTwitter];
AFJSONRequestOperation *operation;
operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *req, NSHTTPURLResponse *responce, id jsonObject) {
NSLog(#"Responce: %#",jsonObject);
self.resultsTwitter = [jsonObject objectForKey:#"results"];
[self.tableView reloadData];
}
failure:^(NSURLRequest *req, NSHTTPURLResponse *responce, NSError *error, id jsonObject) {
NSLog(#"Recieved an HTTP %d", responce.statusCode);
NSLog(#"The error was: %#",error);
}];
[operation start];
I may not have understood your question correctly, but as long as resultsTwitter is a NSMutableArray, you can add an object (in your case an NSDictionary with a single KVP) after it is initially populated.
Something like:
[resultsTwitter addObject:[NSDictionary dictionaryWithObjectsAndKeys:
#"twitter", #"newsSource",
nil]];
Example of instantiating a variable that can be accessed inside a block:
__block NSString *newssource = #"";
NSURLRequest *request = [NSURLRequest requestWithURL:urlTwitter];
AFJSONRequestOperation *operation;
operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *req, NSHTTPURLResponse *responce, id jsonObject) {
NSLog(#"Responce: %#",jsonObject);
self.resultsTwitter = [jsonObject objectForKey:#"results"];
[self.tableView reloadData];
newssource = #"twitter";
}
failure:^(NSURLRequest *req, NSHTTPURLResponse *responce, NSError *error, id jsonObject) {
NSLog(#"Recieved an HTTP %d", responce.statusCode);
NSLog(#"The error was: %#",error);
}];
[operation start];
Create a Model class to encapsulate the behavior of all News Items.
This pattern is used in the AFNetworking example app, with each App.net post corresponding to a model object, which is initialized from JSON. I would strongly recommend against using a mutable dictionary rather than a model object as a means of representing items.

AFNetworking+UIImageView placeholder image shows up, but not URL image

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.

Resources