I have the following code that fetches some data from a php script. I'm finding that it blocks the interface so that the progress indicator doesn't spin.
Is there an easy way to send this to the background so that the UI is free to do whatever it needs? I was under the impression that using blocks would have achieved this but I was obviously wrong (learning Obj-C one step at a time!)
Thanks!
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:
self.user.username, #"username",
self.user.password, #"password",
nil];
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:
[NSURL URLWithString:#"http://domain.com"]];
// Show animated progress indicator, etc
[self enbleLoadingState:YES];
[client postPath:#"/en/api/login" parameters:params success:^(AFHTTPRequestOperation *operation, id response) {
[self enbleLoadingState:NO];
// Do stuff
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self enbleLoadingState:NO];
// Show error
}];
This code should work, the request loads in the background. The problem must be somewhere else.
I don't know where the time is spent on the main thread, maybe in enbleLoadingState:? The method starting the request should return immediately. Test this by inserting a NSLog at the start and the end of the method and inside the success-block. The NSLog in the success-block should print out last.
Related
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
Here's what I'm doing: I'm processing a login that requires manipulating the data afterwards then fires off a new view. I want to do an async request since the server isn't always immediately responsive (and I don't want a crash because I held up the main thread with a synchronous connection)
This is what it looks like now:
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *dataReturn, NSData *data, NSError *error)
{
//blah blah working code
//once data is received, it's processed and I want to call a new view
viewController.hidesBottomBarWhenPushed = YES;
viewController.name = returnUserData[0];
viewController.userID = returnUserData[1];
viewController.role = returnUserData[2];
viewController.sID = returnUserData[3];
[self.navigationController pushViewController:viewController animated:YES];
NSLog(#"Pushed new view controller.");//*/
}];//end async request
Now my problem is that it's not actually visually pushing the view. I can see my NSLog responses are working correctly (the new view immediately responds with "Hello, [name]"), but visually nothing is showing up - This is a problem.
That's fine though, I decided to instead separate the code and try to run the view transition on the main thread
This is an adaptation of I've seen posted online:
NSLog(#"init NSURL response");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(#"Begin async request");
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *dataReturn, NSData *data, NSError *error)
{
NSLog(#"inside async");
if (error)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Error" message:#"There was an error communicating with the server. Please try again." delegate:nil cancelButtonTitle:#"Dismiss" otherButtonTitles:nil];
[alert show];
[alert release];
checkField.text = #"";
}
else
{
//NSLog(#"raw: %# - filtered: %#",dataReturn, (NSString *)dataReturn);
NSDictionary *response = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
rawJSONData = [response objectForKey:#"d"];
NSLog(#"Set jsondata");
}
NSLog(#"ended async");
}];//end async request
NSLog(#"Beginning main thread work");
dispatch_sync(dispatch_get_main_queue(), ^{
//not important for the example
So what it's doing is giving me this:
2014-03-28 21:53:37.059 myApp[652:60b] init NSURL response
2014-03-28 21:53:37.059 myApp[652:3507] Begin async request
2014-03-28 21:53:37.059 myApp[652:3507] Beginning main thread work
It's skipping over the embedded async request entirely.
So what I'm left with is two off-the-main-thread-solutions that aren't doing what I want:
I want to run NSURLConnection off the main thread, I'm getting back JSON data I need to parse, and I want to wait until I get & parse that data before transitioning views.
Is that I'm trying to do possible? Or are my eyes glazed over and I'm just not seeing something I should be?
In your first example, you can probably fix it by either using [NSOperationQueue mainQueue] for the queue rather than what I presume is some background queue. Or dispatch your push to the main queue. UI updates always should be done on the main queue.
A couple of thoughts regarding your second example:
You don't have to dispatch your sendAsynchronousRequest request to a background queue, because it will do it asynchronously already. Wrapping it in a dispatch_async is redundant.
If you want to initiate some transition to the next view once you've successfully parsed the JSON, then put that code to transition to the next view inside the completionHandler block, right where you parse the JSON, not after the sendAsynchronousRequest.
You can use your queue object if you want, but then make sure you dispatch your UIAlertView and your "transition to view controller" code back to the main queue (because all UI updates should take place on the main queue). Frankly, it's easier to just specify the mainQueue as the queue for the completionHandler (because nothing you have there is so time consuming as to warrant the use of a background queue).
Merely as a matter of convention, I'd suggest leaving the NSURLResponse object called response, and rename that other variable.
You might want to include more error checking (e.g. was the JSON successfully parsed). There are many server errors that would not result in the NSError object of the completionHandler to be set (e.g. that's just for basic internet connectivity problems, not random server problems).
Thus, this yields:
NSLog(#"Issuing asynchronous request");
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
NSLog(#"Received response");
if (error)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Error" message:#"There was an error communicating with the server. Please try again." delegate:nil cancelButtonTitle:#"Dismiss" otherButtonTitles:nil];
[alert show];
[alert release];
checkField.text = #"";
}
else
{
//NSLog(#"raw: %# - filtered: %#",dataReturn, (NSString *)dataReturn);
NSError *parseError = nil;
NSDictionary *responseObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:&parseError];
if (responseObject)
{
rawJSONData = [responseObject objectForKey:#"d"];
NSLog(#"Set jsondata");
// initiate the transition to the new view controller here
}
else
{
NSLog(#"JSON parse failed: %#", parseError);
NSLog(#"string from server: %#", [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]);
}
}
NSLog(#"ended async");
}];//end async request
Clearly, you should be checking the response and that the JSON parsing
I have written method -(void) getStatus:(NSString*) url with return type,
-(NSString) getStatus:(NSString*) statusUrl
{
NSURL *urlObj = [NSURL URLWithString:[statusUrl
stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSError *err;
NSString *response = [NSString stringWithContentsOfURL: urlObj encoding:
NSUTF8StringEncoding error:&err];
return response;
}
But stringWithContentsOfURL is performing operation in main Thread, So while calling this method application struck for a second.
So I need to perform stringWithContentsOfURL function in background thread and after getting the response i want to return the response.
My current code:
-(NSString) getStatus:(NSString*) statusUrl
{
NSURL *urlObj = [NSURL URLWithString:[statusUrl
stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSError *err;
NSOperationQueue *rbQueue = [[[NSOperationQueue alloc] init] autorelease];
[rbQueue addOperationWithBlock:^{
NSString *response = [NSString stringWithContentsOfURL: urlObj
encoding: NSUTF8StringEncoding error:&err];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
return response;
}];
}];
return #"";
}
But now me receiving a empty string #"", I can not receive response i got from server. Is there any way to do the same task..
Have you noticed that there should be some changes with this approach to complete the task? You hardly can use getters this way because of the nature of asynchronous methods. Or the getters will block the main thread in their turn.
To avoid this I could recommend you to use NSNotification to update the UI after you complete the server request in the background thread;
or change your getter method definition to pass the result from the background thread to the main thread, but asynchronously:
- (void)getStatusAsychronously:(NSString *)statusUrl withCompletionBlock:(void(^)(NSString *result))completionBlock;
or even consider subclassing NSOperation object for server request. The NSOperation subclasses are really handy with NSOperationQueue instances and could provide some more useful features like the cancellation of operation.
I'm using Kiwi to write the tests for my app.
I wrote tests to test against my API. I was guided by this example in the documentation for testing asynchronous calls:
https://github.com/allending/Kiwi/wiki/Asynchronous-Testing
My tests are long, so I made a simplified version of my issue:
describe(#"My Class Name", ^{
context(#"populate", ^{
it(#"download the content", ^{
__block NSString *testResponseObject = nil;
__block NSError *testError = nil;
MyClient *apiClient = [MyClient sharedClient];
NSMutableURLRequest *request = [apiClient requestWithMethod:#"DELETE" path:#"my/path" parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
testResponseObject = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
testError = error;
}];
[apiClient enqueueHTTPRequestOperation:operation];
[[expectFutureValue(testResponseObject) shouldEventuallyBeforeTimingOutAfter(100)] equal:#"Expected Content"];
[[expectFutureValue(testError) shouldEventuallyBeforeTimingOutAfter(100)] shouldBeNil];
});
});
});
The thing is that if everything works as expected & the operation succeeds the failure block never gets called & instead of nil for NSError I get KWAsyncVerifier.
I'm guessing that's because Kiwi waits for the block where testError is referenced to be executed which never happens & that's why I have KWAsyncVerifier stuck into testError instead of nil.
Is there any alternative how to test this out?
My first recommendation is that you should not test your libraries. From what I read in your example, you are basically checking that AFHTTPRequestOperation is working as documented, but that’s not your responsability to test. You should test that you invoke AFNetworking correctly, and that given an responseObject or an error, your code behaves as you expect.
Anyway, about what you are seeing, you have two “shoulds” in the same line: shouldEventually and shouldBeNil; they use to have beNil matcher, which is unavailable in 2.1, and I think they are bringing back. You can find the discussion in https://github.com/allending/Kiwi/issues/293
Maybe you can try the following to make sure that the failure branch is not taken:
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
testResponseObject = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// This will fail if we ever reach this branch
[error shouldBeNil];
}];
[apiClient enqueueHTTPRequestOperation:operation];
[[expectFutureValue(testResponseObject) shouldEventuallyBeforeTimingOutAfter(100)] equal:#"Expected Content"];
The shouldEventuallyBeforeTimingOutAfter will keep the test case “alive” waiting to check the response, but if you ever go through the failure branch, the other expectation will fail (and also the one in response will fail after 100 seconds). Hope it helps.