Has anyone successfully implemented the "open" action for a file provider app extension? I've gotten as far as being able to read a file when the user initially selects the file in the document picker extension (essentially, this is the "import" action). But anything beyond that fails. Here are the issues I've run into:
NSFileCoordinator
.startAccessingSecurityScopedResource
returns NO
.bookmarkDataWithOptions:
, I get back Error Domain=NSCocoaErrorDomain Code=260 "The operation couldn’t be completed. (Cocoa error 260.)".Here is the template that is created for startProvidingItemAtURL:
when a file provider extension is created:
- (void)startProvidingItemAtURL:(NSURL *)url completionHandler:(void (^)(NSError *))completionHandler {
// Should ensure that the actual file is in the position returned by URLForItemWithIdentifier:, then call the completion handler
NSError* error = nil;
__block NSError* fileError = nil;
NSData * fileData = [NSData data];
// TODO: get the contents of file at <url> from model
[self.fileCoordinator coordinateWritingItemAtURL:url options:0 error:&error byAccessor:^(NSURL *newURL) {
[fileData writeToURL:newURL options:0 error:&fileError];
}];
if (error!=nil) {
completionHandler(error);
} else {
completionHandler(fileError);
}
}
But the extension deadlocks when I use the file coordinator. Also, the documentation for startProvidingItemAtURL:
says "Note
Do not use file coordination inside this method." so I've taken it out.
In the other app, this is what I am doing to read that file for the first time and then create a bookmark for it:
// Start accessing the security scoped resource.
[url startAccessingSecurityScopedResource];
void (^accessor)(NSURL *) = ^void(NSURL *url) {
// If the file is missing, create a default here. This really should be done inside
// the FileProvider method startProvidingItemAtURL:. Unfortunately, that method does
// not get called unless we use use the file coordinator, which can deadlock the app.
if (![url checkResourceIsReachableAndReturnError:nil]) {
// TODO: Create a real default file here.
[[NSFileManager defaultManager] createFileAtPath:url.path
contents:nil
attributes:nil];
}
// TODO: Do something with this file.
};
#ifdef USE_FILE_COORDINATOR
NSFileCoordinator *fileCoordinator = [NSFileCoordinator new];
[fileCoordinator coordinateReadingItemAtURL:url
options:NSFileCoordinatorReadingWithoutChanges
error:NULL
byAccessor:accessor];
#else
accessor(url);
#endif
// Store a bookmark for the url in the defaults so we can use it later.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSError *error = nil;
NSURLBookmarkCreationOptions options = 0;
#ifdef NSURLBookmarkCreationWithSecurityScope
options |= NSURLBookmarkCreationWithSecurityScope;
#endif
NSData *bookmarkData = [url bookmarkDataWithOptions:options
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
if (error) {
NSLog(@"ERROR: %@", error);
}
[defaults setObject:bookmarkData forKey:@"BookmarkDataKey"];
// Stop accessing the security scoped resource.
[url stopAccessingSecurityScopedResource];
And finally, to use the bookmark later, I am doing the following:
// Get the bookmark from the defaults file.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *bookmarkData = [defaults objectForKey:@"BookmarkDataKey"];
if (bookmarkData) {
// Convert the bookmark into a URL.
NSError *error;
BOOL bookmarkIsStale;
NSURLBookmarkResolutionOptions options = NSURLBookmarkResolutionWithoutUI;
#ifdef NSURLBookmarkResolutionWithSecurityScope
options |= NSURLBookmarkResolutionWithSecurityScope;
#endif
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmarkData
options:options
relativeToURL:nil
bookmarkDataIsStale:&bookmarkIsStale
error:&error];
// Get the data from the URL.
BOOL securitySucceeded = [url startAccessingSecurityScopedResource];
if (securitySucceeded) {
NSString *message = [NSString stringWithFormat:@"Random number: #%d", arc4random() % 10000];
NSData *fileData = [NSKeyedArchiver archivedDataWithRootObject:message];
NSError *fileError = nil;
[fileData writeToURL:url options:0 error:&fileError];
[url stopAccessingSecurityScopedResource];
}
}
The second app also sometimes deadlocks if I use file coordination. So should I just not use file coordination in the second app as well? The problem is that if I don't use file coordination, then startProvidingItemAtURL:
in the File Provider extension never seems to get called.
Also, the documentation says to use NSURLBookmarkCreationWithSecurityScope
but this is undefined for iOS. The same goes for NSURLBookmarkResolutionWithSecurityScope
. Should I just use the OS X values or just not use them?
In the end, I think I've got it working by removing file coordination everywhere and ignoring the security scope bookmark constants. Here is what I used for startProvidingItemAtURL:
in the file provider extension:
- (void)startProvidingItemAtURL:(NSURL *)url completionHandler:(void (^)(NSError *))completionHandler {
// If the file doesn't exist then create one.
if (![url checkResourceIsReachableAndReturnError:nil]) {
__block NSError *fileError = nil;
NSString *message = @"This is a test message";
NSData *fileData = [NSKeyedArchiver archivedDataWithRootObject:message];
[fileData writeToURL:url options:0 error:&fileError];
completionHandler(fileError);
}
}
In the other app, this is what I am doing to read that file for the first time and then create a bookmark for it:
// Start accessing the security scoped resource.
[url startAccessingSecurityScopedResource];
// If the file is missing, create a default here. This really should be done inside
// the FileProvider method startProvidingItemAtURL:. Unfortunately, that method does
// not get called unless we use use the file coordinator, which can deadlock the app.
if (![url checkResourceIsReachableAndReturnError:nil]) {
// TODO: Create a real default file here.
[[NSFileManager defaultManager] createFileAtPath:url.path
contents:nil
attributes:nil];
// TODO: Do something with this file.
// Store a bookmark for the url in the defaults so we can use it later.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSError *error = nil;
NSURLBookmarkCreationOptions options = 0;
#ifdef NSURLBookmarkCreationWithSecurityScope
options |= NSURLBookmarkCreationWithSecurityScope;
#endif
NSData *bookmarkData = [url bookmarkDataWithOptions:options
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
if (error) {
NSLog(@"ERROR: %@", error);
}
[defaults setObject:bookmarkData forKey:@"BookmarkDataKey"];
// Stop accessing the security scoped resource.
[url stopAccessingSecurityScopedResource];
And finally, to use the bookmark later, I am doing the following:
// Get the bookmark from the defaults file.
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *bookmarkData = [defaults objectForKey:@"BookmarkDataKey];
if (bookmarkData) {
// Convert the bookmark into a URL.
NSError *error;
BOOL bookmarkIsStale;
NSURLBookmarkResolutionOptions options = NSURLBookmarkResolutionWithoutUI;
#ifdef NSURLBookmarkResolutionWithSecurityScope
options |= NSURLBookmarkResolutionWithSecurityScope;
#endif
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmarkData
options:options
relativeToURL:nil
bookmarkDataIsStale:&bookmarkIsStale
error:&error];
// Get the data from the URL.
BOOL securitySucceeded = [url startAccessingSecurityScopedResource];
if (securitySucceeded) {
NSString *message = [NSString stringWithFormat:@"Random number: #%d", arc4random() % 10000];
NSData *fileData = [NSKeyedArchiver archivedDataWithRootObject:message];
NSError *fileError = nil;
[fileData writeToURL:url options:0 error:&fileError];
[url stopAccessingSecurityScopedResource];
}
}