I have a class which manages all calls to an api.
It has a method to manage this, lets call this callAPIMethod:
This method accepts a success and fail block.
Inside this method, I call uploadTaskWithRequest to make a call to an API.
Within the uploadTaskWithRequest completion handler I'd like to (depending on the result) pass results back through to either the success or fail blocks.
I'm having some issues with this. It works and is keeping everything super tidy but when I call callAPIMethod using the success/fail blocks it's locking up the UI/MainThread rather than being asynchronous as I'd expect.
How should I go about implementing this pattern? Or is there a better way to go about it?
I don't need to support pre-iOS7.
Thanks
Edit: Basic implementation discussed above.
- (void)callApiMethod:(NSString *)method withData:(NSString *)requestData as:(kRequestType)requestType success:(void (^)(id responseData))success failure:(void (^)(NSString *errorDescription))failure {
[redacted]
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session uploadTaskWithRequest:request
fromData:postData
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
failure(error.description);
} else {
NSError *jsonError;
id responseData = [NSJSONSerialization
JSONObjectWithData:data
options:kNilOptions
error:&jsonError];
if (jsonError) {
failure(jsonError.description);
} else {
success(responseData);
}
}
}];
[task resume];
}
CallAPI method, used as follows (from a UITableViewController):
[apiController callApiMethod:#"users.json?action=token"
withData:loginData
as:kRequestPOST
success:^(id responseData) {
if ([responseData isKindOfClass:[NSDictionary class]]) {
if ([responseData objectForKey:#"token"]) {
//Store token/credentials
} else if ([responseData objectForKey:#"error"]) {
//Error
[self displayErrorMessage:[responseData objectForKey:#"error"]];
return;
} else {
//Undefined Error
[self displayErrorMessage:nil];
return;
}
} else {
//Error
[self displayErrorMessage:nil];
return;
}
//If login success
}
failure:^(NSString *errorDescription) {
[self displayErrorMessage:errorDescription];
}];
Your NSURLSession code looks fine. I'd suggest adding some breakpoints so you can identify if it is deadlocking somewhere and if so, where. But nothing in this code sample would suggest any such problem.
I would suggest that you ensure that all UI calls are dispatched back to the main queue. This NSURLSessionUploadTask completion handler may be called on a background queue, but all UI updates (alerts, navigation, updating of UIView controls, etc.) must take place on the main queue.
Related
I have a use case for AFNetworking to behave synchronously (details below). How can I achieve this?
Here is my code snippet, which I've simplified as much as possible.
I would like to return the success response, but I only ever get nil (because the function returns before the block is called).
- (id)sendForUrl:(NSURL *)url {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
__block id response;
[manager GET:url.absoluteString parameters:nil success: ^(AFHTTPRequestOperation *operation, id responseObject) {
response = responseObject;
NSLog(#"JSON: %#", responseObject);
} failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
return response;
}
Details
So the reason I need this to behave synchronously is because I am building a pod that will bootstrap an application at start up. The boot strapping hits a services and saves a bunch of values locally. These values are then used for the current session and not changed. If the values change, the user will get a strange experience, so its important I avoid this.
If the service is down, that's okay. We'll use default values or looks for some saved values from a previous session, but whatever happens, we don't want the experience for the user to change in the session.
(This is an engine for A/B testing and experimentation - if that helps you 'get' the use case).
Ignoring the fact that we generally do not want to make synchronous network requests, the traditional solution is to use semaphores:
- (id)sendForUrl:(NSURL *)url {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block id response;
[manager GET:url.absoluteString parameters:nil success: ^(AFHTTPRequestOperation *operation, id responseObject) {
response = responseObject;
NSLog(#"JSON: %#", responseObject);
dispatch_semaphore_signal(semaphore);
} failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return response;
}
There are two issues here:
I know you said you didn't want "to see some complex dispatch style things", but with due deference to others, a semaphore is better than a while loop that is spinning, polling until some value changes.
Note, assuming you're calling this method from the main thread, you must set the completionQueue to be some other, background queue. By default, the AFHTTPRequestOperationManager will use the main queue for those completion blocks. And if you block that thread waiting until the completion blocks to run on that same thread, you'll deadlock (i.e. your app will freeze).
For the sake of completeness, I'll point out that invariably, when someone asks "how to I make some asynchronous method behave synchronously", the correct answer is, "you don't". Instead, you generally adopt asynchronous patterns, for example changing the method to take a block parameter:
- (void)sendForUrl:(NSURL *)url completionHandler:(void (^)(id responseObject, NSError *error))completionHandler {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
[manager GET:url.absoluteString parameters:nil success: ^(AFHTTPRequestOperation *operation, id responseObject) {
if (completionHandler) {
completionHandler(responseObject, nil);
}
} failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
if (completionHandler) {
completionHandler(nil, error);
}
}];
}
You'd then call it like so, providing a completion handler block:
[object sendForURL:url completionHandler:^(id responseObject, NSError *error) {
// use responseObject and/or error here
}];
// don't use them here
It sounds like there's a part of your app that can't run until a starting request is done. But there's also a part that can run (like the part that starts the request). Give that part a UI that tells the user that we're busy getting ready. No blocking the UI.
But if you must, and if AFNetworking doesn't provide a blocking version of a request (kudos to them for that), then you could always block the old fashioned way...
- (void)pleaseDontUseThisIdea {
__block BOOL thePopeIsCatholic = YES;
[manager GET: ....^{
// ...
thePopeIsCatholic = NO;
}];
while (thePopeIsCatholic) {}
}
I am very new to the concept of asynchronous programming, but I get the general gist of it (things get run in the backround).
The issue I'm having is I have a method which utilizes AFNetworking 2.0 to make an HTTP post request, and it works for the most part.
However, I can't figure out how to get the method to actually return the value received from the response as the method returns and THEN gets the value from the response.
-(int) registerUser
{
self.responseValue = 000; //Notice I set this to 000 at start of method
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
NSDictionary *parameters = #{ #"Username": #"SomeUsername" };
[manager POST:#"http://XXX/register"
parameters:parameters
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLog(#"JSON: %#", responseObject);
NSError *err = nil;
self.responseValue = [[responseObject objectForKey:#"res"] intValue];
//Note: This^ value returns 99 and NSLogs confirm this
}
failure:^(AFHTTPRequestOperation *operation, NSError *err)
{
NSLog(#"Error: %#", err);
}];
return self.responseValue; //This will return 000 and never 99!
}
Whats the 'proper' way to handle this situation? I've heard whispers of using a 'callback', but I don't really understand how to implement that in this situation.
Any guidance or help would be awesome, cheers!
The issue is that the POST runs asynchronously, as you point out, so you are hitting the return line well before the responseValue property is actually set, because that success block runs later. Put breakpoints/NSLog statements in there, and you'll see you're hitting the return line first.
You generally do not return values from an asynchronous methods, but rather you adopt the completion block pattern. For example:
- (void)registerUserWithCompletion:(void (^)(int responseValue, NSError *error))completion
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
NSDictionary *parameters = #{ #"Username": #"SomeUsername" };
[manager POST:#"http://XXX/register"
parameters:parameters
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLog(#"JSON: %#", responseObject);
int responseValue = [[responseObject objectForKey:#"res"] intValue];
if (completion)
completion(responseValue, nil);
}
failure:^(AFHTTPRequestOperation *operation, NSError *err)
{
NSLog(#"Error: %#", err);
if (completion)
completion(-1, err); // I don't know what you want to return if it failed, but handle it appropriately
}];
}
And then, you could use it as follows:
[self registerUserWithCompletion:^(int responseValue, NSError *error) {
if (error)
NSLog(#"%s: registerUserWithCompletion error: %#", __FUNCTION__, error);
else
NSLog(#"%d", responseValue);
// do whatever you want with that responseValue here, inside the block
}];
// Needless to say, don't try to use the `responseValue` here, because
// `registerUserWithCompletion` runs asynchronously, and you will probably
// hit this line of code well before the above block is executed. Anything
// that is dependent upon the registration must called from within the above
// completion block, not here after the block.
Note, I'd suggest you retire that responseValue property you had before, because now that you're using completion blocks, you get it passed back to you via that mechanism, rather than relying on class properties.
Check this one and use search ;-))
Getting variable from the inside of block
its a lot of duplicates already!
:-)
I’m sorry if this question is too basic, but I can’t seem to find a an answer online.
I want to fetch the JSON result and have them returned with the class method below. But as you can see, by fetching the JSON in the block method, I don’t have a way to return them as result.
What is the correct way to to return them as NSDictionary from inside block method, or is there any other way to simplify this?
+ (NSDictionary *) fetchtPostsCount:(NSString *) count
page: (NSString *) page {
NSDictionary *requestParameter = [[NSDictionary alloc] initWithObjectsAndKeys:count, #"count", page, #"page", nil];
[[self sharedClient] GET:#"get_recent_posts"
parameters:requestParameter success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"%#", [responseObject objectForKey:#"posts"]);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"%#", error);
}];
return nil;
}
AFNetworking executes requests on different thread, and calls the success or failure block when its done. Conceptually, you can imagine that your fetchPostsCount method will have already completed and returned its value by the time request is finished.
You almost certainly want it to work that way. Running the request on another thread and NOT waiting for it, allows your main UI thread to continue processing events and rendering screen updates. You don't want to get in the way of those things, or the user (and iOS) will get unhappy.
However, if you insist on waiting for the request to complete before returning, you could set a flag to monitor the status of the request, and then wait on that flag until the request is complete:
BOOL requestComplete = NO;
id requestResponseObject = nil;
[[self sharedClient] GET:#"get_recent_posts"
parameters:requestParameter success:^(NSURLSessionDataTask *task, id responseObject) {
requestResponseObject = responseObject;
requestComplete = YES;
NSLog(#"%#", [responseObject objectForKey:#"posts"]);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
requestComplete = YES;
NSLog(#"%#", error);
}];
while (!requestComplete)
{
// Tie up the thread, doing nothing...
}
// Proceed
I am a little bit confused about objective c programming with blocks.
for example Here is a method:
in the .h
- (void)downloadDataWithURLString:(NSString *)urlString
completionHandler:(void(^) (NSArray * response, NSError *error))completionHandler;
in the .m:
- (void)downloadedDataURLString:(NSString *)urlString
completionHandler:(void (^)(NSArray *, NSError *))completionHandler {
// some things get done here. But what!?
}
My main questions is.... how do I implement this completion handler? What variables would be returned with the array and error? it is one area for the code but how do I tell it what to do when it is completed?
It's up to the caller to supply code to be run by the method (the body of the block). It's up to the implementor to invoke that code.
To start with a simple example, say the caller just wanted you to form an array with the urlString and call back, then you would do this:
- (void)downloadedDataURLString:(NSString *)urlString
completionHandler:(void (^)(NSArray *, NSError *))completionHandler {
NSArray *callBackWithThis = #[urlString, #"Look ma, no hands"];
completionHandler(callBackWithThis, nil);
}
The caller would do this:
- (void)someMethodInTheSameClass {
// make an array
[self downloadedDataURLString:#"put me in an array"
completionHandler:^(NSArray *array, NSError *error) {
NSLog(#"called back with %#", array);
}];
}
The caller will log a two item array with #"put me in an array" and #"Look ma, no hands". In a more realistic example, say somebody asked you to call them back when you're finished downloading something:
- (void)downloadedDataURLString:(NSString *)urlString
completionHandler:(void (^)(NSArray *, NSError *))completionHandler {
// imagine your caller wants you to do a GET from a web api
// stripped down, that would look like this
// build a request
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// run it asynch
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
// imagine that the api answers a JSON array. parse it
NSError *parseError;
id parse = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&parseError];
// here's the part you care about: the completionHandler can be called like a function. the code the caller supplies will be run
if (!parseError) {
completionHandler(parse, nil);
} else {
NSLog(#"json parse error, error is %#", parseError);
completionHandler(nil, parseError);
}
} else {
NSLog(#"error making request %#", error);
completionHandler(nil, error);
}
}];
// remember, this launches the request and returns right away
// you are calling the block later, after the request has finished
}
While I can't be entirely sure without seeing any more details about the method, or its exact implementation I suspect this: this methods creates a new background thread, retrieves the data from the server and converts the JSON/XML to an NSArray response. If an error occurred, the error object contains a pointer to the NSError. After doing that, the completion handler is called on the main thread. The completion handler is the block in which you can specify which code should be executed after attempting to retrieve the data.
Here is some sample code on how to call this method to get you started:
[self downloadDataWithURLString:#"http://www.google.com"
completionHandler:^(NSArray *response, NSError *error) {
if (! error) {
// Do something awesome with the 'response' array
} else {
NSLog(#"An error occured while downloading data: %#", error);
}
}];
I have a set of asynchronous calls being spawned using NSInvocationOperation:
- (void)listRequestQueue:(StoreDataListRequest *)request {
[openListRequests addObject:request];
NSInvocationOperation *requestOp = [[NSInvocationOperation alloc]
initWithTarget:self
selector:#selector(listRequestStart:)
object:request];
[opQueue addOperation:requestOp];
[requestOp release];
}
- (void)listRequestStart:(StoreDataListRequest *)request {
if(self.resourceData == nil) {
//TODO fail appropriately...
return;
}
StoreDataListResponse *response = [self newListResponseForProductID:request.productID];
[self performSelectorOnMainThread:#selector(listRequestFinish:)
withObject:response waitUntilDone:NO];
[response release];
[self performSelectorOnMainThread:#selector(cleanUpListRequest:)
withObject:request waitUntilDone:NO];
}
- (void)listRequestFinish:(StoreDataListResponse *)response {
[self.delegate storeData:self didReceiveListResponse:response];
}
- (StoreDataListResponse *)newListResponseForProductID:(NSString *)productID {
CollectionData *data = [self.resourceData dataForProduct:productID];
if(data == nil) {
//TODO do something
}
StoreDataListResponse *response = [[StoreDataListResponse alloc] init];
response.productID = productID;
if(productID != data.productID) {
//TODO fail; remove product from list
}
response.name = NSLocalizedString(#"Loading...", #"Loading message");
response.blurb = NSLocalizedString(#"Waiting for response from server", #"Waiting for website to respond");
return response;
}
For each of the TODOs in the above code, I should resolve the issue and let any handlers know that things have failed and why. Looking at the NSError class and documentation, it appears to be the appropriate answer but I am having trouble figuring out how to get to work with NSInvocationOperation and performSelectorOnMainThread:withObject:waitUntilDone:. I can certainly get the NSErrors out of the newListResponseForProductID: method by changing it to something like this:
- (StoreDataListResponse *)newListResponseForProductID:(NSString *)productID error:(NSError **)error;
How do I get the error generated back into the main thread so I can deal with the failed request?
The easiest way to run any code on the main thread is to use GCD and blocks on iOS 4 and above. Like this:
dispatch_async(dispatch_get_main_queue(), ^(void) {
// Your code to run on the main thread here,
// any variables in scope like NSError is captured.
});
Or use dispatch_sync() if you know you are on a background thread and want the block to complete on the main thread before you continue.