UIDocumentPicker and NSFileCoordinator


When using UIDocumentPickerViewController with either UIDocumentPickerModeOpen or UIDocumentPickerModeMove you are given access to files outside the app’s sandbox. You are then required to use a NSFileCoordinator when accessing files since other processes might potentially be accessing them simultaneously / concurrently.

Using UIDocument you get this, and more, for free. But what if you do not want your logic in the UI layer. Say you want the UI layer just to fetch the file URL by presenting the DocumentPicker and then pass it down to a lower layer, a layer that is unaware of any UI (UIKit or AppKit).

Create a Document class

It’s pretty easy to make one’s own UIDocument class for the purpose of handling access with NSFileCoordinator (Of course, UIDocument does much more). The initializer could be declared in the same way:

- (instancetype)initWithFileURL:(NSURL*)fileURL
{
    if (![fileURL isFileURL]) {
        return nil;
    }
    
    self = [super init];

    if (self) {
        _fileURL = [fileURL copy];
    }

    return self;
}

URLs pointing to files outside the app’s sandbox are security scoped. Copying a security-scoped NSURL retains the security scope of the original.

Before accessing the URL, startAccessingSecurityScopedResource must be called; this method should always be paired with stopAccessingSecurityScopedResource:

- (void)ensureStartAccessingSecurityScopedResource
{
    if ([self.fileURL startAccessingSecurityScopedResource]) {
        _isFileURLSecurityScoped = YES;
    }
}

- (void)stopAccessingSecurityScopedResource
{
    if (_isFileURLSecurityScoped)
    {
        [self.fileURL stopAccessingSecurityScopedResource];
        _isFileURLSecurityScoped = NO;
    }
}

stopAccessingSecurityScopedResource should be called as soon as possible after file access is done. One app is not allowed to have too many security scoped files open for access as the same time, startAccessingSecurityScopedResource will return NO. To be on the safe side, one could make a safty call in dealloc.
objc - (void)dealloc { [self stopAccessingSecurityScopedResource]; }

For simplicity, we are not tracking the files state here, just reading it once into memory. So we do not pass a NSFilePresenter when creating the NSFileCoordinator.

- (NSFileCoordinator*)fileCoordinator
{
    if (!_fileCoordinator) {
        _fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
    }
    return _fileCoordinator;
}

The NSFileCoordinator needs a queue when coordinating file access asynchronously (asynchronous access is new in iOS8).

- (NSOperationQueue*)queue
{
    if (!_queue) {
        _queue = [[NSOperationQueue alloc] init];
    }
    return _queue;
}

An example of coordinated access might look like this.

- (void)dataWithCallbackQueue:(dispatch_queue_t)callbackQueue completionHandler:(void (^)(NSData *data, NSError *error))completionHandler
{
    [self ensureStartAccessingSecurityScopedResource];

    NSFileAccessIntent *readingIntent = [NSFileAccessIntent readingIntentWithURL:self.fileURL
                                                                         options:NSFileCoordinatorReadingWithoutChanges];
                                                                                       
    [self.fileCoordinator coordinateAccessWithIntents:@[readingIntent]
                                                queue:self.queue
                                           byAccessor:^(NSError *error) {

                                               NSData *data;
                                               
                                               if (!error)
                                               {
                                                   // Always get URL from access intent. It might have changed.
                                                   NSURL *safeURL = readingIntent.URL;
                                                   
                                                   // Read URL                                                   
                                                   data = [self readFromURL:safeURL error:&error];
                                               }
                                               
                                               [self stopAccessingSecurityScopedResource];

                                               if (callbackQueue != NULL) 
                                               {
                                                   dispatch_async(callbackQueue, ^{ 
                                                       completionHandler(data, error); 
                                                   });
                                               }
                                               else 
                                               {
                                                   completionHandler(data, error);
                                               }                                               
                                           }];
}

Performing a coordinated read with the NSFileCoordinator also might trigger the download of the file if for example the user picked an iCloud document that is not yet locally on the device. The accessor block above will be called after the file is downloaded. Testing with a 600 MB file it took about 20 mins until the accessor block was invoked.

Other document providers such as Box or Google Drive downloads the document before the UIDocumentPickerViewController is dismissed and it’s delegate provides the file URL. So in these cases the accessor block will be called as soon as it is safe for the app to access the file, probably instantly.


Note that startAccessingSecurityScopedResource and stopAccessingSecurityScopedResource of NSURL and NSFileCoordinator’s coordinateAccessWithIntents:queue:byAccessor are all new in iOS8.