1

I'm trying to fetch the contents of a URL from a method called connect. It reads its configuration settings and attempts to fetch the URL. It appears to need the run loop to execute. I though it would just execute once and be done. It seems that I don't need to have that run loop running for any reason other than to fetch this URL once, whenever connect is called. Is there a better way do do this?

- (BOOL) connect 
{
    // read serverName and eventId from preferences
    NSMutableArray* preferences;
    preferences = [NSMutableDictionary dictionaryWithContentsOfFile:@"/tmp/wt.plist"];

    // if these values aren't nil or blank
    if ([preferences valueForKey:@"serverAddress"] && [preferences valueForKey:@"eventId"]) {

        [self setServerAddress:@"172.16.28.210"];
        [self setEventId:@"OT-3037009"];
    } else{
        // return some kind of error message
    }

    NSLog(@"VideoDataURL: %@", [self getVideoDataURL]);

    // grab the URL and query the server

    NSURL *myURL = [NSURL URLWithString:@"http://localhost"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:myURL
                                                           cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
                                                       timeoutInterval:2];

    __unused NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];

    [[NSRunLoop currentRunLoop] run];

    return TRUE;
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    // This method is called when the server has determined that it
    // has enough information to create the NSURLResponse.

    // It can be called multiple times, for example in the case of a
    // redirect, so each time we reset the data.

    // receivedData is an instance variable declared elsewhere.
    [incomingData setLength:0];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    /* 
     * Called each time a chunk of data arrives
     */

    NSLog(@"received %lu bytes", [data length]);

    // Create a mutable data if it doesn't already exist
    if (!incomingData) {
        incomingData = [[NSMutableData alloc] init];
    }

    [incomingData appendData:data];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 
{
    /*
     * Called if the connection fails
     */

    NSLog(@"connection failed: %@", [error localizedDescription]);  
    incomingData = nil;
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection 
{
    /*
     * Called when the last chunk has been processed
     */

    NSLog(@"Got it all!");

    NSString *string = [[NSString alloc] initWithData:incomingData encoding: NSUTF8StringEncoding];

    incomingData = nil;

    NSLog(@"response is: %@", string);
}
Adam
  • 913
  • 1
  • 9
  • 26
  • The [documentation](https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/URLLoadingSystem/Tasks/UsingNSURLConnection.html) says "The download starts immediately upon receiving the initWithRequest:delegate: message." This doesn't seem to be the case here. I have must be missing something that is forcing my NSURLConnection to wait for the run loop. – Adam May 10 '12 at 14:33
  • I add an explicit call to `start` anyway. According to the documentation, it may be unnecessary, but I figure it can't hurt! – Ash Furrow May 10 '12 at 15:57
  • 1
    The issue was that my application is a command line application, which doesn't have a run loop by default. I had to start one to get it to work properly. – Adam May 12 '12 at 13:20

3 Answers3

10

If you're starting the connection off of the main thread, yes, this is correct. I just use GCD to create and start the connection on the main thread to avoid having to think about run loops.

dispatch_async(dispatch_get_main_queue(), ^{
    NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
    [conn start];
});

Works from anywhere and you don't have to keep track of run loops or threads.

Ash Furrow
  • 12,391
  • 3
  • 57
  • 92
  • Oh my god, I have been trying to figure out the cause of my NSURLConnection not receiving it's delegate method callbacks all day... thank you – Jason May 31 '12 at 04:38
  • Doesn't seem to work, still don't get the - (void) connectionDidFinishLoading:(NSURLConnection *)connection callback... – jjxtra Jul 31 '12 at 20:16
  • Are you responding to the other delegate methods? Is it possible the connection is failing? This can happen because, for instance, the system doesn't know how to handle the URL. Try putting this in a synchronous request to see if it works there, and if it doesn't, then it won't work with the asynchronous ones, either. – Ash Furrow Aug 01 '12 at 01:40
  • 2
    Isn't running this on the main (i.e. UI thread) a bad idea? It could work for short requests, but for longer ones that will be downloading lots of data, you'd probably want to keep it on a separate thread/runloop. – kevlar Feb 18 '13 at 04:31
  • Tried and at least the connectionDidFinishLoading is calling now. But either the parameters that I sent in not honoured or what the webData return undesirable result. My webservice is not passing back the correct value, somewhere is not correct. – TPG Sep 13 '15 at 13:36
  • This fixed the problem of the connection not being made for me, thank you! – Marcus Roberts Oct 10 '16 at 16:09
3

I would make sure you manually tell the runLoop of the worker thread you're spawning the connection on to run until the connection is finished. If you do not do this, Cocoa will automatically put the thread to sleep, hence why your delegate methods seem to never receive a response.

// The following is delcared within the scope of a NSThread, NSOperation, or GCD Block
//
// The NSURLConnection object and it's initialization/start methods
// |
// V
// Above the method call to the thread's runLoop.
//
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:<a time interval>];
// I use a minute, or 60.0, before I manually cancel the connection
// and send an error message back to the caller.

^Is the method you should call after you spawn your connection. It should give you the results you're looking for.

Keep this structure in mind when you're building asynchronous server calls/tasks:

Thread

RunLoop

Task

Each asynchronous task needs its own RunLoop in order for events to be continuously polled during the duration of its existence. Each RunLoop needs to exist within the scope of a thread in order for its associated task(s) to be executed.

You can build an asynchronous networking task manually using NSThread/NSRunLoop/NSURLConnection, Grand Central Dispatch, or even NSOperation/NSOperationQueue. I prefer Operations/Queues because I feel they are more Object-Oriented, and block-based programming can be kind of tricky (which is the structure you'd be writing your request in if you just used "dispatch_" calls). When it all comes down to it, write your threading model based on your programming preference. Each approach will give you the same result.

Also, do not use dispatch_sync unless you're performing the portion of a task that requires synchronization, or else you will lock-up the UI. Mastery of RunLoops is necessary in order to create a truly asynchronous model for your application.

Good luck!

P.S. NSURLConnection is already built to run asynchronously, but, to create a truly asynchronous networking model, your application will need to be spawning connections on threads separate from the Main Thread.

user298261
  • 422
  • 3
  • 17
  • Could you expand your answer a bit with some code? I get the gcd and the NSURLConnection delegate part. How to fit NSRunLoop into that is not clear to me. – user965972 Mar 01 '15 at 15:26
  • The code I wrote needs to be placed beneath the asynchronous URL call if that connection is being invoked somewhere besides the MainThread. If you don't do this, you run the risk of Cocoa putting the thread to sleep after the connection is spawned and potentially missing updates to that connection's state. As a result, you will not retrieve the data you intended to get/post/update/etc. I'll update my post according to your request. – user298261 Jun 04 '15 at 20:36
0

I don't think synchronous requests need a run loop:

NSURLResponse* response = nil;
NSData* data = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&response error:nil];
Arlen Anderson
  • 2,486
  • 2
  • 25
  • 36
  • 2
    This is true, but the question is specifically asking about asynchronous calls, which are far more powerful than the `sendSynchronousRequest:returningResponse:error:` method. – Ash Furrow May 10 '12 at 15:56