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:
- Implement lifecycle properties:
isExecuting
, isFinished
- 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 NSURLConnection
s 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
NSThread
s.
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 ;)