0

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
4ntoine
  • 19,816
  • 21
  • 96
  • 220
  • Yup, that won't work in a single threaded test case. That won't work single threaded at all. How are you making progress callbacks to a thread that's blocked? – quellish Aug 06 '14 at 06:55
  • as far as i understand downloading is done asynchronously (if using initWithRequest:request delegate:self];). it means it uses GCD thread to perform downloading actually and callbacks are invoked in worker thread. i can't understand why i can't block main thread and sit waiting for background thread to download URL. download method is designed to be run in background thread so it does not block main app thread – 4ntoine Aug 06 '14 at 07:00
  • dispatch_async will not solve your problem. The nature of the problem you're trying to solve is itself asynchronous. The download has to happen asynchronously, with the progress updating a thread that is not blocked waiting for the download. If that were implemented, to test it you would need to use the newer asynchronous testing infrastructure that's in beta, or something equivalent. What you're trying to do is best done using NSProgress, but it's poorly documented. – quellish Aug 06 '14 at 07:01
  • "i can't understand why i can't block main thread and sit waiting for background thread to download URL". You can't do that and update progress on the main thread if the main thread is blocked. And you don't want to block the main thread to begin with. Consider calling back to the main thread with the result instead. https://developer.apple.com/library/ios/qa/qa1693/_index.html – quellish Aug 06 '14 at 07:04
  • i can update progress using NSLog() and i don't need to touch main thread to display progress. i believe delegate callbacks should be invoked in worker thread and i DO want to block the main thread. otherwise the test will be finished and i can't see downloading progress and results – 4ntoine Aug 06 '14 at 07:08
  • "otherwise the test will be finished and i can't see downloading progress and results" This is why you need the asynchronous test support in Xcode 6. – quellish Aug 06 '14 at 07:10
  • so the reason is that main thread should NOT be blocked to download even asynchronously? – 4ntoine Aug 06 '14 at 07:12

2 Answers2

0

Your wait loop lock the CPU main thread to 100%, patch your wait loop with :

...
// block the thread until downloading finished
while (![self isFinished]) 
{ 
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
};
...
tdelepine
  • 1,986
  • 1
  • 13
  • 19
  • now CPU load is 0% and energy impact is "Low" but callbacks are not invoked anyway. i've tried to download the file on the URL manually and it can be downloaded – 4ntoine Aug 06 '14 at 06:52
  • no delegate is called ? Have you placed breakpoint in each of them will. – tdelepine Aug 06 '14 at 07:00
  • sure. i've already added NSLog() to each of them and still nothing – 4ntoine Aug 06 '14 at 07:05
0

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]];
}
4ntoine
  • 19,816
  • 21
  • 96
  • 220