Search code examples
iosobjective-cnsurlconnectionnsurlsession

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.


Solution

  • 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.)