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.