Fredlo2010 Fredlo2010 - 6 months ago 36
iOS Question

Wait for NSURLSessionDataTask to come back

I am new to Objective C and iOS development in general. I am trying to create an app that would make an http request and display the contents on a label.

When I started testing I noticed that the label was blank even though my logs showed that I had data back. Apparently this happens because the the response is not ready when the label text gets updated.

I put a loop on the top to fix this but I am almost sure there's got to be a better way to deal with this.

ViewController.m

- (IBAction)buttonSearch:(id)sender {

HttpRequest *http = [[HttpRequest alloc] init];
[http sendRequestFromURL: @"https://en.wiktionary.org/wiki/incredible"];

//I put this here to give some time for the url session to comeback.
int count;
while (http.responseText ==nil) {
self.outputLabel.text = [NSString stringWithFormat: @"Getting data %i ", count];
}

self.outputLabel.text = http.responseText;

}


HttpRequest.h

#import <Foundation/Foundation.h>

@interface HttpRequest : NSObject

@property (strong, nonatomic) NSString *responseText;

- (void) sendRequestFromURL: (NSString *) url;
- (NSString *) getElementBetweenText: (NSString *) start andText: (NSString *) end;

@end


HttpRequest.m

@implementation HttpRequest

- (void) sendRequestFromURL: (NSString *) url {

NSURL *myURL = [NSURL URLWithString: url];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL: myURL];
NSURLSession *session = [NSURLSession sharedSession];


NSURLSessionDataTask *task = [session dataTaskWithRequest: request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
self.responseText = [[NSString alloc] initWithData: data
encoding: NSUTF8StringEncoding];
}];
[task resume];

}


Thanks a lot for the help :)

Update



After reading a lot for the very useful comments here I realized that I was missing the whole point. So technically the NSURLSessionDataTask will add task to a queue that will make the call asynchronously and then I have to provide that call with a block of code I want to execute when the thread generated by the task has been completed.

Duncan thanks a lot for the response and the comments in the code. That helped me a lot to understand.

So I rewrote my procedures using the information provided. Note that they are a little verbose but, I wanted it like that understand the whole concept for now. (I am declaring a code block rather than nesting them)

HttpRequest.m

- (void) sendRequestFromURL: (NSString *) url
completion:(void (^)(NSString *, NSError *))completionBlock {

NSURL *myURL = [NSURL URLWithString: url];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL: myURL];
NSURLSession *session = [NSURLSession sharedSession];


NSURLSessionDataTask *task = [session dataTaskWithRequest: request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

//Create a block to handle the background thread in the dispatch method.
void (^runAfterCompletion)(void) = ^void (void) {
if (error) {
completionBlock (nil, error);
} else {
NSString *dataText = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
completionBlock(dataText, error);
}
};

//Dispatch the queue
dispatch_async(dispatch_get_main_queue(), runAfterCompletion);
}];
[task resume];

}


ViewController.m

- (IBAction)buttonSearch:(id)sender {

NSString *const myURL = @"https://en.wiktionary.org/wiki/incredible";

HttpRequest *http = [[HttpRequest alloc] init];

[http sendRequestFromURL: myURL
completion: ^(NSString *str, NSError *error) {
if (error) {
self.outputText.text = [error localizedDescription];
} else {
self.outputText.text = str;
}
}];
}


Please feel free to comment on my new code. Style, incorrect usage, incorrect flow; feedback is very important in this stage of learning so I can become a better developer :)

Again thanks a lot for the replies.

Answer

Rewrite your sendRequestFromURL function to take a completion block:

- (void) sendRequestFromURL: (NSString *) url
    completion:  (void (^)(void)) completion
{
    NSURL *myURL = [NSURL URLWithString: url];
    NSURLRequest *request = [[NSURLRequest alloc] initWithURL: myURL];
    NSURLSession *session = [NSURLSession sharedSession];


    NSURLSessionDataTask *task = [session dataTaskWithRequest: request
      completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) 
      {
        self.responseText = [[NSString alloc] initWithData: data
          encoding: NSUTF8StringEncoding];
        if (completion != nil)
        {
           //The data task's completion block runs on a background thread 
           //by default, so invoke the completion handler on the main thread
           //for safety
           dispatch_async(dispatch_get_main_queue(), completion);
        }
      }];
      [task resume];
}

Then, when you call sendRequestFromURL, pass in the code you want to run when the request is ready as the completion block:

[self.sendRequestFromURL: @"http://www.someURL.com&blahblahblah",
  completion: ^
  {
    //The code that you want to run when the data task is complete, using
    //self.responseText
  }];

  //Do NOT expect the result to be ready here. It won't be.

The code above uses a completion block with no parameters because your code saved the response text to an instance variable. It would be more typical to pass the response data and the NSError as parameters to the completion block. See @Yahoho's answer for a version of sendRequestFromURL that takes a completion block with a result string and an NSError parameter).

(Note: I wrote the code above in the SO post editor. It probably has a few syntax errors, but it's intended as a guide, not code you can copy/paste into place. Objective-C block syntax is kinda nasty and I usually get it wrong the first time at least half the time.)