I have an app that successfully uses the synchronous methods to download files (NSData's initWithContentsOfURL and NSURLConnection's sendSynchronousRequest), but now I need to support large files. This means I need to stream to disk bit by bit. Even though streaming to disk and becoming asynchronous should be completely orthoganal concepts, Apple's API forces me to go asynchronous in order to stream.
To be clear, I am tasked with allowing larger file downloads, not with re-architecting the whole app to be more asynchronous-friendly. I don't have the resources. But I acknowledge that the approaches that depend on re-architecting are valid and good.
So, if I do this:
NSURLConnection* connection = [ [ NSURLConnection alloc ] initWithRequest: request delegate: self startImmediately: YES ];
.. I eventually have didReceiveResponse and didReceiveData called on myself. Excellent. But, if I try to do this:
NSURLConnection* connection = [ [ NSURLConnection alloc ] initWithRequest: request delegate: self startImmediately: YES ];
while( !self.downloadComplete )
[ NSThread sleepForTimeInterval: .25 ];
... didReceiveResponse and didReceiveData are never called. And I've figured out why. Weirdly, the asynchronous download happens in the same main thread that I'm using. So when I sleep the main thread, I'm also sleeping the thing doing the work. Anyway, I have tried several different ways to achieve what I want here, including telling the NSURLConnection to use a different NSOperationQueue, and even doing dispatch_async to create the connection and start it manually (I don't see how this couldn't work - I must not have done it right), but nothing seems to work. Edit: What I wasn't doing right was understanding how Run Loops work, and that you need to run them manually in secondary threads.
What is the best way to wait until the file is done downloading?
Edit 3, working code: The following code actually works, but let me know if there's a better way.
Code executing in the original thread that sets up the connection and waits for the download to complete:
dispatch_queue_t downloadQueue = dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 );
dispatch_async(downloadQueue, ^{
self.connection = [ [ NSURLConnection alloc ] initWithRequest: request delegate: self startImmediately: YES ];
[ [ NSRunLoop currentRunLoop ] run ];
});
while( !self.downloadComplete )
[ NSThread sleepForTimeInterval: .25 ];
Code executing in the new thread that responds to connection events:
-(void)connection:(NSURLConnection*) connection didReceiveData:(NSData *)data {
NSUInteger remainingBytes = [ data length ];
while( remainingBytes > 0 ) {
NSUInteger bytesWritten = [ self.fileWritingStream write: [ data bytes ] maxLength: remainingBytes ];
if( bytesWritten == -1 /*error*/ ) {
self.downloadComplete = YES;
self.successful = NO;
NSLog( @"Stream error: %@", self.fileWritingStream.streamError );
[ connection cancel ];
return;
}
remainingBytes -= bytesWritten;
}
}
-(void)connection:(NSURLConnection*) connection didFailWithError:(NSError *)error {
self.downloadComplete = YES;
[ self.fileWritingStream close ];
self.successful = NO;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
self.downloadComplete = YES;
[ self.fileWritingStream close ];
self.successful = YES;
}