Force wait for NSURLSessionDataTask completion - objective-c

I'm making a URLRequest that sends attempted login information and waits for a response from the web service to find out if the user can/cannot log in.
The way I was hoping to do this was by having the user type in his username & password into two text fields and then press a button, which would call the function below. This function would start an NSURLSessionDataTask and construct a struct with the boolean success/failure of the login and an NSString with the corresponding error message (if any).
The problem is that my function returns the struct before my NSURLSessionDataTask's completion block has finished executing. Is there a way for me to force my program to wait until this task either times out or completes? Alternatively, can I push execution of the completion block onto the main thread & before the function returns?
Thanks! Please let me know if there are any clarifications I need to make!
(Also, I have seen some similar questions circulating around StackOverflow that mention GCD. Is this an overkill solution? None of those questions seem to be talking about quite the same thing, or do so on a level that is higher than my current understanding. I am still very new to Objective-C)
- (struct RequestReport) sendLoginRequest: (NSString*) username withPassword: (NSString *) password
... (creating the request & setting HTTP body)
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error){
NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData: data options:0 error:nil];
success = (BOOL)jsonObject[#"success"];
statusText = (NSString *) jsonObject[#"errors"];
}];
[dataTask resume];
struct RequestReport rr;
rr.status = statusText;
rr.success = success;
return rr;

Your method should look like this:
- (void) sendLoginRequest:(NSString*) username withPassword:(NSString *) password callback:(void (^)(NSError *error, BOOL success))callback
{
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error){
if (error) {
// Handle error
}
else {
callback(error, YES);
}
}];
[dataTask resume];
}
Call this method like so:
[self sendLoginRequest:#"myUsername" password:#"password" callback:^(NSString *error, BOOL success) {
if (success) {
NSLog(#"My response back from the server after an unknown amount of time";
}
}
See Apple's Programming with Objective-C for more reading on blocks and fuckingblocksyntax.com for how to declare blocks.

Related

BREAK statement not in loop or switch in Objective C

I have a set of array which I need to loop and identify each the url domain is passed 200 status code. IF the first index value is pass then I shall break the loop and save the url in local. here is my code:
for (int j = 0; j < items.count; j++){
NSString *urlStr2 = items[j];
[session GET:urlStr2 parameters:nil progress:^(NSProgress * _Nonnull uploadProgress) {
// nil
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
// The domain name request is successful, processing data
NSURL *url = [NSURL URLWithString:urlStr2];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection cancelPreviousPerformRequestsWithTarget:self];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
if ([httpResponse statusCode] == 200){
h5UrlStr = urlStr2;
break;
}
}];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
// Domain name request failed
if (error.code == -1003) {
}
}];
}
But the problem is when I place the break when the condition is meet, it will appear error saying that break statement not in loop or switch Objective C. How should I break the loop?
In this case break will not help you because you are calling it in lambda (anonymous function). It is completely different scope. As I can see in your code you are launching up to items.count parallel requests but you need results of only the first successful.
One possible solution is to assign names to callback functions, get rid of loop and make the same GET call with advanced element counter in case of each failure while j < items.count. In such case all request will be sequential, one after another until some of them succeed. It can take a time.
Another solution is to launch requests in parallel (probably not for all elements of items at once, but for some chunk) and on each successful call check if result was already obtained. If it was, simply ignore current and return. This is faster but wastes bandwidth and computing resources.

Unit Testing with NSURLSession for OCMock

I have a networking class called: ITunesAlbumDataDownloader
#implementation AlbumDataDownloader
- (void)downloadDataWithURLString:(NSString *)urlString
completionHandler:(void (^)(NSArray *, NSError *))completionHandler
{
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
NSArray *albumsArray = [self parseJSONData:data];
completionHandler(albumsArray, error);
}];
[dataTask resume];
}
- (NSArray *)parseJSONData:(NSData *)data {
NSMutableArray *albums = [[NSMutableArray alloc] init];
...
...
// Return the array
return [NSArray arrayWithArray:albums];
}
#end
and i need to create a Unit Test for this which does the following:
The NSURLSession dataTaskWithRequest:completionHandler: response is mocked to contain the fake JSON data i have:
// Expected JSON response
NSData *jsonResponse = [self sampleJSONData];
The returned array from the public method downloadDataWithURLString:completionHandler: response should contain all the albums and nil error.
Other points to bare in mind is that i need to mock NSURLSession with the fake JSON data "jsonResponse" to the downloadDataWithURLString:completionHandler: method WITHOUT invoking an actual network request.
I have tried various different things but i just can not work it out, i think its the combination of faking the request and the blocks which is really confusing me.
Here is two examples of my test method that i tried (i actually tried a lot of other ways also but this is what i have remaining right now):
- (void)testValidJSONResponseGivesAlbumsAndNilError {
// Given a valid JSON response containing albums and an AlbumDataDownloaderTests instance
// Expected JSON response
NSData *jsonResponse = [self sampleJSONDataWithAlbums];
id myMock = [OCMockObject mockForClass:[NSURLSession class]];
[[myMock expect] dataTaskWithRequest:OCMOCK_ANY
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
{
}];
[myMock verify];
}
and
- (void)testValidJSONResponseGivesAlbumsAndNilError {
// Given a valid JSON response containing albums and an AlbumDataDownloaderTests instance
// Expected JSON response
NSData *jsonResponse = [self sampleJSONDataWithAlbums];
id myMock = [OCMockObject mockForClass:[AlbumDataDownloader class]];
[[[myMock stub] andReturn:jsonResponse] downloadDataWithURLString:OCMOCK_ANY
completionHandler:^(NSArray *response, NSError *error)
{
}];
[myMock verify];
}
}
I have a feeling that in both instances I'm probably way off the mark :(
I would really appreciate some help with this.
Thanks.
UPDATE 1:
Here is what i have now come up with but need to know if I'm on the right track or still making a mistake?
id mockSession = [OCMockObject mockForClass:[NSURLSession class]];
id mockDataTask = [OCMockObject mockForClass:[NSURLSessionDataTask class]];
[[mockSession stub] dataTaskWithRequest:OCMOCK_ANY
completionHandler:^(NSData _Nullable data, NSURLResponse Nullable response, NSError * Nullable error)
{
NSLog(#"Response: %#", response);
}];
[[mockDataTask stub] andDo:^(NSInvocation *invocation)
{
NSLog(#"invocation: %#", invocation);
}];
The trick with blocks is you need the test to call the block, with whatever arguments the test wants.
In OCMock, this can be done like this:
OCMStub([mock someMethodWithBlock:([OCMArg invokeBlockWithArgs:#"First arg", nil])]);
This is convenient. But…
The downside is that the block will be invoked immediately when someMethodWithBlock: is called. This often doesn't reflect the timing of production code.
If you want to defer calling the block until after the invoking method completes, then capture it. In OCMock, this can be done like this:
__block void (^capturedBlock)(id arg1);
OCMStub([mock someMethodWithBlock:[OCMArg checkWithBlock:^BOOL(id obj) {
capturedBlock = obj;
return YES;
}]]);
// ...Invoke the method that calls someMethodWithBlock:, then...
capturedBlock(#"First arg"); // Call the block with whatever you need
I prefer to use OCHamcrest's HCArgumentCaptor. OCMock supports OCHamcrest matchers, so I believe this should work:
HCArgumentCaptor *argument = [[HCArgumentCaptor alloc] init];
OCMStub([mock someMethodWithBlock:argument]);
// ...Invoke the method that calls someMethodWithBlock:, then...
void (^capturedBlock)(id arg1) = argument.value; // Cast generic captured argument to specific type
capturedBlock(#"First arg"); // Call the block with whatever you need

Wait for AFNetworking completion block before continuing (i.e. Synchronous)

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) {}
}

NSURLSession uploadTaskWithRequest - use block within completion handler

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.

How do I use this block method?

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