Search code examples
iosobjective-cxcodensurlconnectionnsurlconnectiondelegate

Synchronous downloading with NSURLConnection and progress callback?


I'm trying to implement synchronous downloading with progress callback with NSURLConnection. When [connection start] is invoked, nothing happens - delegate callback methods are not just invoked (i'm testing on OSX in XCTestCase). What's wrong?

// header
@interface ASDownloadHelper : NSObject <NSURLConnectionDelegate, NSURLConnectionDataDelegate>
{
    NSMutableData *_receivedData;
    NSUInteger _expectedBytes;
    id<ASDownloadHelperListener> _listener;
    NSError *_error;
    BOOL _finished;
    id _finishedSyncObject;
}

- (void) download: (NSString*)url file:(NSString*)file listener:(id<ASDownloadHelperListener>)listener;

@end

// impl
@implementation ASDownloadHelper

// delegate

- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [_receivedData setLength:0];
    _expectedBytes = [response expectedContentLength];
}

- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [_receivedData appendData:data];

    int percent = round( _receivedData.length * 100.0 / _expectedBytes );
    [_listener onDownloadProgress:_receivedData.length total:_expectedBytes percent:percent];
}

- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    _error = error;
    [self setFinished:YES];
}

- (NSCachedURLResponse *) connection:(NSURLConnection*)connection
                   willCacheResponse:(NSCachedURLResponse*)cachedResponse {
    return nil;
}

- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
    [self setFinished: YES];
}

- (BOOL) isFinished {
    @synchronized(_finishedSyncObject) {
        return _finished;
    }
}

- (void) setFinished: (BOOL)finished {
    @synchronized(_finishedSyncObject) {
        _finished = finished;
    }
}

// ---

- (void) download: (NSString*)downloadUrl file:(NSString*)file listener:(id<ASDownloadHelperListener>)listener {
    _listener = listener;
    _finished = NO;
    _finishedSyncObject = [[NSObject alloc] init];
    _error = nil;

    NSURL *url = [NSURL URLWithString:downloadUrl];

    NSURLRequest *request = [NSURLRequest requestWithURL:url
                                                cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
                                            timeoutInterval:30];
    _receivedData = [[NSMutableData alloc] initWithLength:0];
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
                                                                  delegate:self];
    [connection start];

    // block the thread until downloading finished
    while (![self isFinished]) { };

    // error?
    if (_error != nil) {
        @throw _error;
        return;
    }

    // success
    [_receivedData writeToFile:file atomically:YES];
    _receivedData = nil;
}

@end

Solution

  • Thanks to quellish i've found out that invocation queue should not be blocked as callback invocations (delegate methods) are done in invoker thread context. In my case i was running it in main test thread so i had to do workaround (and sleep in main thread for few seconds to let downloading finish):

    - (void)testDownload
    {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
            // ...
            [_downloadHelper download:repositoryUrl file:downloadedFile listener:downloadlistener];
    
            // progress callbacks are invoked in this thread context, so it can't be blocked
    
            // ...
            XCTAssertNotNil( ... );
        });
    
        // block main test queue until downloading is finished
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
    }