AFImageRequestOperation wait until finished - objective-c

i have a little problem with this, I'm loading an Image from a Url like this:
+ (void)getImageFromURL:(NSString *)imageFilename urlMode:(NSString *)mode block:(id (^)(UIImage *responseImage))aImage {
NSURL *url = [NSURL URLWithString:[mainURL stringByAppendingString:mode]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"GET"];
AFImageRequestOperation *requestOperation = [AFImageRequestOperation imageRequestOperationWithRequest:request
imageProcessingBlock:nil
cacheName:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
aImage(image);
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error)
{
// manage errors
}];
[[[NSOperationQueue alloc]init] addOperation:requestOperation];
}
I'm trying to set an iVar UIImage *userAvatar to the response from this request, but the problem is, since its an async request I'm not getting the iVar set before my Code moves on, so my iVar is empty when I'm accessing it and passing it to another method.

That's the nature of asynchronous programming! You are going to have to redesign the dependencies on userAvatar to take into account that it's availability is nondeterministic.
So, rather than having your operation's success block simply set the userAvatar ivar, it takes care of whatever needs to happen once that image is available. For example if you want to set a UIImageView's image, then in your success block:
dispatch_async(dispatch_get_main_queue(), ^{
myImageView.image = image;
});
(Without knowing the details of your goals and details of your implementation, this is just a "for example...")

You forgot to add [requestOperation start]; at the end.

Related

Setting object property inside a block trouble on Objective-C

I'm starting to learn Objective-C for iOS Development, and a got a issue that is driving me crazy.
All that I want is to do a request, retrieve e JSON and then set this JSON into an instance property.
-(NSArray *) retrieveAtEndpoint:(NSString *)endpointURL withRootNode:(NSString *)rootNode
{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat: endpointURL, fuegoWSURL]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSDictionary *dict = (NSDictionary *) JSON;
[self setJSONObjectsCollection: [dict objectForKey:rootNode]];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Communication Error: %#", error);
}];
[operation start];
return _JSONObjectsCollection;
}
-(void) setJSONOBjectsCollectionAttribute: (NSArray *) arrayWithCollection
{
NSLog(#"Outside Method %#", arrayWithCollection);
self.JSONObjectsCollection = arrayWithCollection;
}
However, my self.JSONObjectsCollection property are ok inside the block, but outside is always null.
Can you help me guys ?
It's because the setting of JSONObjectsCollection happens asynchronously. So your method is returning JSONObjectsCollection before it is set.
Thus, it might look like:
- (void)retrieveAtEndpoint:(NSString *)endpointURL withRootNode:(NSString *)rootNode
{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat: endpointURL, fuegoWSURL]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSDictionary *dict = (NSDictionary *) JSON;
[self setJSONObjectsCollection: [dict objectForKey:rootNode]];
// do here whatever you want to do now that you have your array, e.g.
//
// [self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Communication Error: %#", error);
}];
[operation start];
}
Note, retrieveAtEndpoint now has a void return type, but in the completion block, I'm invoking whatever code you want to perform once the JSON objects collection has been updated.
If this is a method inside your model object, but you want to provide an interface by which the view controller can supply a block of code that should be executed upon successful retrieval of the JSON, use a completion block:
- (void)retrieveAtEndpoint:(NSString *)endpointURL withRootNode:(NSString *)rootNode completion:(void (^)(NSError *error))completion
{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat: endpointURL, fuegoWSURL]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSDictionary *dict = (NSDictionary *) JSON;
[self setJSONObjectsCollection: [dict objectForKey:rootNode]];
if (completion)
{
completion(nil);
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
if (completion)
{
completion(error);
}
}];
[operation start];
}
Or, if you want to simplify your use of a block parameter, you can define a type for the completion block at the start of your model object's .h file (before the #interface block):
typedef void (^RetrievalCompleteBlock)(NSError *);
And then the method is simplified a bit:
- (void)retrieveAtEndpoint:(NSString *)endpointURL withRootNode:(NSString *)rootNode completion:(RetrievalCompleteBlock)completion
{
// the code here is like it is above
}
Anyway, regardless of whether you use the typedef or not, the view controller could do something like:
ModelObject *object = ...
NSString *rootNode = ...
[object retrieveAtEndpoint:url withRootNode:rootNode completion:^(NSError *error) {
if (error)
{
// handle the error any way you want, such as
NSLog(#"%s: retrieveAtEndPoint error: %#", __FUNCTION__, error);
}
else
{
// do whatever you want upon successful retrieval of the JSON here
}
}];
The details here will vary based upon how your view controller is accessing the model object, knows that the root node should be, etc. I often will include another parameter to my completion block which is the data being retrieved, but given that you updated your model object and can access it that way, perhaps that's not necessary. I simply don't have enough details about your implementation to know what is right here, so I kept my implementation as minimalist as possible.
But hopefully this illustrates the idea. Give your retrieveAtEndpoint method a completion block parameter, which lets the view controller specify what it wants to do upon completion (or failure) of the communication with the server.

Edit image before cache with AFNetworking

I am loading a bunch of images using AFNetworking and I would like to scale and apply rounded corners to these images before AFNetworking caches them.
I started out scaling and applying rounded corners to the images each time they were loaded but the completion block will also be called when the image is loaded from the cache and therefore this uses too many resources when a user scrolls a collection view filled with images.
[self.imageView setImageWithURLRequest:[NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReturnCacheDataElseLoad
timeoutInterval:10.0f]
placeholderImage:kVideoCollectionViewCellVideoImagePlaceholder
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
/**
* The image is edited here and this block is called
* when the image is loaded from web and from the cache.
*/
[self.imageView setImage:image];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
DDLogError(#"%#", error);
}];
AFNetworking seems to provide a great cache for my use, especially when I enable disk caching therefore I would like to use it but I can't figure out if there's a way to edit the image before it is cached.
Does anyone know if this is possible and if so, how it can be done?
After posting the question it hit me that I may have too look in another direction that using the UIImageView+AFNetworking category. Using the AFImageRequestOperation directly solved the problem.
__weak NZVideoCollectionViewCell *weakSelf = self;
NSURLRequest *request = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReturnCacheDataElseLoad
timeoutInterval:10.0f];
AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request imageProcessingBlock:^UIImage*(UIImage *image) {
/**
* Edit image.
*/
return editedImage;
}
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
[weakSelf.imageView setImage:image];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
DDLogError(#"%#", error);
}];
[operation start];

can't access self?

In this function I get xml through a kiss xml function called AFKissXMLRequestOperation. But since it is void, I can't access the XMLDocument unless I NSLog it, but that isn't useful when I need to access the XML. So, I try to set it as a variable of self in order to access it in other functions. If I NSLog self.xmlDocument inside of the block, it works. But, when I NSLog it outside the block in the call NSLog(#"self!%#", [self.xmlDocument XMLStringWithOptions:DDXMLNodePrettyPrint]); it is NULL. So how can I access self.XMLDocument?
-(id)xmlRequest:(NSString *)xmlurl
{
AFKissXMLRequestOperation* operation= [AFKissXMLRequestOperation XMLDocumentRequestOperationWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:xmlurl]] success:^(NSURLRequest *request, NSHTTPURLResponse *response, DDXMLDocument *XMLDocument) {
NSLog(#"kiss operation %#", [XMLDocument XMLStringWithOptions:DDXMLNodePrettyPrint]);
self.xmlDocument=XMLDocument;
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, DDXMLDocument *XMLDocument) {
NSLog(#"Failure!");
}];
[operation start];
NSLog(#"self!%#", [self.xmlDocument XMLStringWithOptions:DDXMLNodePrettyPrint]);
return self.xmlDocument;
}
NSURLRequest performs asynchroneously, so you will have to review either the way your code is organised, or using a synchroneous network operation.
The error in your above code is that since NSURLRequest performs asynchroneously, the
NSLog(#"self!%#", [self.xmlDocument XMLStringWithOptions:DDXMLNodePrettyPrint]);
return self.xmlDocument;
is performed before the operation has finished, thus returning nil.
Do you really need to return xmlDocument ? I don't think so, because you set it as a property. My guess is that in the success block, (where you set self.xmlDocument=XMLDocument; ) you could actually process the xmlDocument as you want to or call a method that do such.
Hope that helps
I believe AFKissXMLRequestOperation is an asynchronous operation, so you should do what you need to do with the response inside the success block. The success block can certainly call another function if you wish to keep the response handling separate. If you need to pass the response back to another class, you can do that by setting up your own delegate protocol / property or by using blocks yourself:
The delegate protocol:
#protocol XMLResponseHandlerDelegate <NSObject>
- (void)handleResponseXML:(XMLDocument *)xmlDoc;
#end
And then call it:
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:xmlurl]];
AFKissXMLRequestOperation* operation= [AFKissXMLRequestOperation XMLDocumentRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, DDXMLDocument *xmlDocument)
{
NSLog(#"kiss operation %#", [XMLDocument XMLStringWithOptions:DDXMLNodePrettyPrint]);
[self.delegate handleResponseXML:xmlDocument];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, DDXMLDocument *XMLDocument)
{
NSLog(#"Failure!");
}];

Delay when updating view in the completionHandler of an async HTTP request

In my app when the user presses a button I start a HTTP asynchronous request (using [NSURLConnection sendAsynchronousRequest...]) and change the text of UILabel in the completionHandler block. This change, however, does not take place when the request is concluded and instead happens around 2-3 seconds later. Below is a code snippet that results in this behavior.
- (IBAction)requestStuff:(id)sender
{
NSURL *url = [NSURL URLWithString:#"http://stackoverflow.com/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error)
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
exampleLabel.text = [NSString stringWithFormat:#"%d", httpResponse.statusCode];
}];
}
A similar behavior happens when I attempt to create an UIAlertView inside the completionHandler.
- (IBAction)requestStuff:(id)sender
{
NSURL *url = [NSURL URLWithString:#"http://stackoverflow.com/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error)
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if ([httpResponse statusCode] == 200) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"It worked!"
message:nil
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}
}];
}
A small difference, though, is that the screen dims when [alert show] is executed. The alert itself only appears 2-3 seconds later like in the previous scenario.
I'm guessing this is related to how the UI is handled by the app's threads, but I'm not sure. Any guidance on why the delay happens will be greatly appreciated.
According to The Apple Docs.
Threads and Your User Interface
If your application has a graphical user interface, it is recommended that you receive user-related events and initiate interface updates from your application’s main thread. This approach helps avoid synchronization issues associated with handling user events and drawing window content. Some frameworks, such as Cocoa, generally require this behavior, but even for those that do not, keeping this behavior on the main thread has the advantage of simplifying the logic for managing your user interface.
Calling your UI updates on the main thread would solve this problem. Surround your UI code with a call to the main thread (below).
dispatch_async(dispatch_get_main_queue(), ^{
exampleLabel.text = [NSString stringWithFormat:#"%d", httpResponse.statusCode];
});
There are other ways to do calls on the main thread, but using the simpler GCD commands would do the job. Again, see the Threaded Programming Guide for more info.
This could happen because all UI stuff should be called in a main queue. Try this:
- (IBAction)requestStuff:(id)sender
{
NSURL *url = [NSURL URLWithString:#"http://stackoverflow.com/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error)
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
dispatch_async(dispatch_get_main_queue(), ^{
exampleLabel.text = [NSString stringWithFormat:#"%d", httpResponse.statusCode];
});
}];
}
You can try to create a method that sets the text, and inside the block you call:
[self performSelectorOnMainThread:#selector(mySelector) withObject:nil waitUntilDone:NO];
The selector will call and executed on main thread. Hope this help....

Waiting for completion block to complete in an AFNetworking request

I am making a JSON request with AFNetworking and then call [operation waitUntilFinished] to wait on the operation and the success or failure blocks. But, it seems to fall right though - in terms of the log messages, I get "0", "3", "1" instead of "0", "1", "3"
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"http://google.com"]];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
httpClient.parameterEncoding = AFFormURLParameterEncoding;
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:#"query", #"q", nil];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:[url path] parameters:params];
NSLog(#"0");
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *innerRequest, NSHTTPURLResponse *response, id JSON) {
NSLog(#"1");
gotResponse = YES;
} failure:^(NSURLRequest *innerRequest, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"2");
gotResponse = YES;
}];
NSLog(#"Starting request");
[operation start];
[operation waitUntilFinished];
NSLog(#"3");
This works by using AFNetworking to set up the requests, but making a synchronous call then handling the completion blocks manually. Very simple. AFNetworking doesn't seem to support this https://github.com/AFNetworking/AFNetworking/wiki/AFNetworking-FAQ, though the work around is simple enough.
#import "SimpleClient.h"
#import "AFHTTPClient.h"
#import "AFJSONRequestOperation.h"
#import "AFJSONUtilities.h"
#implementation SimpleClient
+ (void) makeRequestTo:(NSString *) urlStr
parameters:(NSDictionary *) params
successCallback:(void (^)(id jsonResponse)) successCallback
errorCallback:(void (^)(NSError * error, NSString *errorMsg)) errorCallback {
NSURLResponse *response = nil;
NSError *error = nil;
NSURL *url = [NSURL URLWithString:urlStr];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
httpClient.parameterEncoding = AFFormURLParameterEncoding;
NSMutableURLRequest *request = [httpClient requestWithMethod:#"POST" path:[url path] parameters:params];
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if(error) {
errorCallback(error, nil);
} else {
id JSON = AFJSONDecode(data, &error);
successCallback(JSON);
}
}
#end
That should (almost) work. Your call to
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:[url path] parameters:params];
should probably not pass [url path] to the path: parameter. In AFNetworking land, that path is everything after the base url (for example the base url could be "http://google.com" and the path "/gmail" or whatever).
That being said, it's probably not a good idea to make the asynchronous operation into a thread-blocking synchronous operation with waitUntilFinished, but I'm sure you have your reasons... ;)
I just had the same problem and found a different solution. I had two operations that depend on each other, but can load in parallel. However, the completion block of the second operation can not be executed before the completion block of the first one has finished.
As Colin pointed out, it might be a bad choice to make a web request block. This was essential to me, so I did it asynchronously.
This is my solution:
// This is our lock
#interface SomeController () {
NSLock *_dataLock;
}
#end
#implementation
// This is just an example, you might as well trigger both operations in separate
// places if you get the locking right
// This might be called e.g. in awakeFromNib
- (void)someStartpoint {
AFJSONRequestOperation *operation1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:[NSURLRequest requestWithURL:url1]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id data) {
// We're done, we unlock so the next operation can continue its
// completion block
[_dataLock unlock];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id data) {
// The request has failed, so we need to unlock for the next try
[_dataLock unlock];
}];
AFJSONRequestOperation *operation2 = [AFJSONRequestOperation JSONRequestOperationWithRequest:[NSURLRequest requestWithURL:url2]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id data) {
// The completion block (or at least the blocking part must be run in a
// separate thread
[NSThread detachNewThreadSelector:#selector(completionBlockOfOperation2:) toTarget:self withObject:data];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id data) {
// This second operation may fail without affecting the lock
}];
// We need to lock before both operations are started
[_dataLock lock];
// Order does not really matter here
[operation2 start];
[operation1 start];
}
- (void)completionBlockOfOperation2:(id)data {
// We wait for the first operation to finish its completion block
[_dataLock lock];
// It's done, so we can continue
// We need to unlock afterwards, so a next call to one of the operations
// wouldn't deadlock
[_dataLock unlock];
}
#end
Use Delegate method call
Put the method inside block which will call itself when downloading/uploading completes.