Christian Beer

Christian Beer

Dropbox File Download With NSOperation

Dropbox provides a rather great SDK for iOS that wraps the REST API in some convenient Cocoa classes. Using that SDK you can very easily log in to Dropbox, upload files, grap metadata (folder content and the like) and download files.

Extending iVocabulary with a Dropbox interface, I wanted to add the option to upload and download ProVoc files to/from Dropbox. But ProVoc files are so called bundles, that are more folders than files (they look like files in the finder, though). So downloading these “files” is a combination of getting metadata and downloading the files. (Disclaimer: Dropbox doesn’t like you to walk the folder tree recursively! But as the ProVoc bundles are at most 1 level deep, I think it’s ok).

My first attempt to do the download of these folders was very naive: get the metadata of the ProVoc file (folder) and get all files / folders recursively. As I said: the folders are not deeply nested, but there can be a lot of media files: images and audio. So this approach led to a huge amount of open connections in parallel what led to errors.

My new approach involves NSOperationQueue, a very handy class, that provides a queue for operations that are processed in parallel. There are some NSOperation implementations for your convenience and you can create your own NSOperation implementations. I found a very short introduction to NSOperationQueue and how it works with NSURLConnection here: Concurrent Operations Demystified. That helped to get it to work very quickly.

Using NSOperationQueue I can now enqueue all file and metadata requests and then wait until all downloads are finished. Let’s look how I did it:

NSOperation

To build your own (asynchronous) NSOperation implementation you need to do the following:

  1. Implement lifecycle properties: isExecuting, isFinished
  2. Implement lifecycle methods: start and finish

The Properties are straight forward to implement:

@interface DropBoxOperation : NSOperation {
    BOOL _isExecuting;
    BOOL _isFinished;
}
@property (readonly) BOOL isExecuting;
@property (readonly) BOOL isFinished;
@end

and

@synthesize isExecuting = _isExecuting;
@synthesize isFinished = _isFinished;

I implemented these in an abstract class called DropBoxOperation that is extended by special operations for downloading metadata and files. These two subclasses implement start and finish.

My operations also provide a delegate mechanism to react on downloaded files and metadata but that is not shown here.

DropBoxDownloadFileOperation

As an example I will show how I implemented start and finish for downloading files. Downloading metadata works nearly the same.

- (void)start {
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(start) 
                               withObject:nil 
                            waitUntilDone:NO];
        return;
    }

    [self willChangeValueForKey:@"isExecuting"];
    _isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];

    _restClient = [[DBRestClient alloc] initWithSession:[DBSession sharedSession]];
    _restClient.delegate = self;

    [_restClient loadFile:_remotePath 
                 intoPath:_localPath];

    if (_restClient == nil)
        [self finish];
}
- (void)finish {
    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];

    _isExecuting = NO;
    _isFinished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}  

#pragma mark DBRestClientDelegate  

- (void) restClient:(DBRestClient *)client loadedFile:(NSString *)destPath {
    // notify delegate
    [self finish];
}
- (void) restClient:(DBRestClient *)client loadFileFailedWithError:(NSError *)error {
    // notify delegate
    _error = [error copy];
    [self finish];
}

The only special part is the first lines of code in the start method: Because the DBRestClient uses NSURLConnection, the code must be run on the main thread. Otherwise the NSURLConnections delegates won’t be called.

The other parts are straight forward: the state is getting updated and the rest-client is used to load the file. After the download has been finished, the delegate methods of the rest-client get called and they call the method finish where the state gets updated accordingly.

How is it used?

In my Dropbox-download manager class, I implemented a method for my convenience that adds a download request to the queue:

- (void) addLoadFile:(NSString*)remotePath {
    NSString *localPath = [self localPathFromRemotePath:remotePath]
    DropBoxDownloadFileOperation *operation = 
      [[DropBoxDownloadFileOperation alloc] initWithRemotePath:remotePath
                                                     localPath:localPath];
    operation.delegate = self;
    [_queue addOperation:operation];
    [operation release];
}

My metadata download operation calls the DBRestClientDelegate method on its delegate. I used that to implement a recursive download of the folders and files (addLoadMetadata: works the same as addLoadFile:):

- (void)restClient:(DBRestClient*)client loadedMetadata:(DBMetadata*)metadata {

    if (metadata.isDirectory) {
        NSString *remotePath = metadata.path;
        NSString *localPath = [self localPathFromRemotePath:remotePath];

        NSError *error = nil;
        [[NSFileManager defaultManager] createDirectoryAtPath:localPath 
                                  withIntermediateDirectories:YES
                                                   attributes:nil
                                                        error:&error;];

        for (DBMetadata *fileMD in metadata.contents) {
            if (fileMD.isDirectory) {
                remotePath = fileMD.path;
                localPath = [self localPathFromRemotePath:remotePath];

                error = nil;
                [[NSFileManager defaultManager] createDirectoryAtPath:localPath 
                                          withIntermediateDirectories:YES
                                                           attributes:nil
                                                                error:&error;];

                [self addLoadMetadata:fileMD.path];
            } else {
                [self addLoadFile:fileMD.path];
            }
        }
    }    
}

(error handling has been striped out for readability).

My findings

I didn’t use NSOperation that much before and I am very sad I didn’t use it! It is such a convenient way to handle asynch tasks without bothering with NSThreads.

The biggest argument for NSOperation/-Queue is the parallelability that you get as a gift. When using NSOperation for tasks, Grand Central Dispatch can decide on which processor the task shall run. With a blink of an eye your app runs on all cores of your iPhone … errr … I mean … Mac ;)