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....
Related
i have the following code to make requests to my api:
-(void)makeApiCall:(NSString *)function params:(NSDictionary *)params notificationName:(NSString *)notificationName
{
NSURL *url = [NSURL URLWithString:kBaseUrl];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
httpClient.parameterEncoding = AFFormURLParameterEncoding;
NSMutableURLRequest *request = [httpClient requestWithMethod:#"POST" path:[NSString stringWithFormat:#"%#/%#",kApiBaseUrl,function] parameters:params];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSLog(#"complete");
//..My code
}];
}
My problem is that, after the NSLog(#"complete"), the app is frozen for 5 seconds..
How can i fix that?
Thanks.
The queue parameter of sendAsynchronousRequest is the queue on which the
completion handler block is dispatched. If you do any UI updates in the completion
handler then you probably should call the method with
queue:[NSOperationQueue mainQueue]
So I am trying to lazyload a user pictures for a custom UITableView (BubbleTableView)
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"GET"];
AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request imageProcessingBlock:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
NSLog(#"Success");
UIBubbleTableViewCell *cell = (UIBubbleTableViewCell *)[self.bubbleTableView cellForRowAtIndexPath:indexPath];
NSBubbleData *data = [[NSBubbleData alloc] init];
data = [cell data];
data.avatar = [UIImage imageNamed:#"send.png"];
[self.profileImages setObject:image forKey:[self.commUsers objectAtIndex:x]];
//Success
[cell setData:data];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
//Failed
NSLog(#"ERROR: %#", response);
}];
[operation start];
I am trying to change the Avatar to another image and I am not using the image pulled from the net rather just an image I have stored locally (for purposes of narrowing image update problem).
So if I put the
UIBubbleTableViewCell *cell = (UIBubbleTableViewCell *)[self.bubbleTableView cellForRowAtIndexPath:indexPath];
NSBubbleData *data = [[NSBubbleData alloc] init];
data = [cell data];
data.avatar = [UIImage imageNamed:#"send.png"];
[self.profileImages setObject:image forKey:[self.commUsers objectAtIndex:x]];
//Success
[cell setData:data];
Inside the AFImageRequestOperation Block, the image doesn't update. However, if I put the exact same code outside the Block, it updates the image. I feel like I am missing something on how Blocks work. How do I fix this?
Thanks!
Try to run the UI code in the block on the main thread:
if ([NSThread isMainThread]) {
// We're currently executing on the main thread.
// We can execute the block directly.
createBubbleTableViewCell();
} else {
//non-blocking call to main thread
dispatch_sync(dispatch_get_main_queue(), createBubbleTableViewCell);
}
The checking if you are on the main thread is just for case - preventing deadlock. Also you can use dispatch_async for blocking call.
The UI code should be run always on the main thread.
I'm using AFNetworking for asynchronous calls to a web service. Some of these calls must be chained together, where the results of call A are used by call B which are used by call C, etc.
AFNetworking handles results of async calls with success/failure blocks set at the time the operation is created:
NSURL *url = [NSURL URLWithString:#"http://api.twitter.com/1/statuses/public_timeline.json"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSLog(#"Public Timeline: %#", JSON);
} failure:nil];
[operation start];
This results in nested async call blocks which quickly becomes unreadable. It's even more complicated when tasks are not dependent on one another and instead must execute in parallel and execution depends on the results of all operations.
It seems that a better approach would be to leverage a promises framework to clean up the control flow.
I've come across MAFuture but can't figure out how best to integrate it with AFNetworking. Since the async calls could have multiple results (success/failure) and don't have a return value it doesn't seem like an ideal fit.
Any pointers or ideas would be appreciated.
I created a light-weight solution for this. It's called Sequencer and it's up on github.
It makes chaining API calls (or any other async code) easy and straightforward.
Here's an example of using AFNetworking with it:
Sequencer *sequencer = [[Sequencer alloc] init];
[sequencer enqueueStep:^(id result, SequencerCompletion completion) {
NSURL *url = [NSURL URLWithString:#"https://alpha-api.app.net/stream/0/posts/stream/global"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
completion(JSON);
} failure:nil];
[operation start];
}];
[sequencer enqueueStep:^(NSDictionary *feed, SequencerCompletion completion) {
NSArray *data = [feed objectForKey:#"data"];
NSDictionary *lastFeedItem = [data lastObject];
NSString *cononicalURL = [lastFeedItem objectForKey:#"canonical_url"];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:cononicalURL]];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
completion(responseObject);
} failure:nil];
[operation start];
}];
[sequencer enqueueStep:^(NSData *htmlData, SequencerCompletion completion) {
NSString *html = [[NSString alloc] initWithData:htmlData encoding:NSUTF8StringEncoding];
NSLog(#"HTML Page: %#", html);
completion(nil);
}];
[sequencer run];
I haven't used it yet, but it sounds like Reactive Cocoa was designed to do just what you describe.
It was not uncommon when using AFNetworking in Gowalla to have calls chained together in success blocks.
My advice would be to factor the network requests and serializations as best you can into class methods in your model. Then, for requests that need to make sub-requets, you can call those methods in the success block.
Also, in case you aren't using it already, AFHTTPClient greatly simplifies these kinds of complex network interactions.
PromiseKit could be useful. It seems to be one of the more popular promise implementations, and others have written categories to integrate it with libraries like AFNetworking, see PromiseKit-AFNetworking.
There is an Objective-C implementation of CommonJS-style promises here on Github:
https://github.com/mproberts/objc-promise
Example (taken from the Readme.md)
Deferred *russell = [Deferred deferred];
Promise *promise = [russell promise];
[promise then:^(NSString *hairType){
NSLog(#"The present King of France is %#!", hairType);
}];
[russell resolve:#"bald"];
// The present King of France is bald!
I haven't yet tried out this library, but it looks 'promising' despite this slightly underwhelming example. (sorry, I couldn't resist).
You can combine NSBlockOperation with semaphore to achieve it:
- (void)loadDataByOrderSuccess:(void (^)(void))success failure:(void (^)(void))failure {
// first,load data1
NSBlockOperation * operation1 = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[self loadData1Success:^{
dispatch_semaphore_signal(sema);
} failure:^{
!failure ?: failure();
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}];
// then,load data2
NSBlockOperation * operation2 = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[self loadData2Success:^{
dispatch_semaphore_signal(sema);
} failure:^{
!failure ?: failure();
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}];
// finally,load data3
NSBlockOperation * operation3 = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[self loadData3Success:^{
dispatch_semaphore_signal(sema);
} failure:^{
!failure ?: failure();
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
!success ?: success();
}];
[operation2 addDependency:operation1];
[operation3 addDependency:operation2];
NSOperationQueue * queue = [[NSOperationQueue alloc] init];
[queue addOperations:#[operation1, operation2, operation3] waitUntilFinished:NO];
}
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.
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.