Search code examples
iosobjective-cios-app-extension

Implementing UIDocumentPickerModeOpen with File Providers


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:

  • The app deadlocks if I use an NSFileCoordinator.
  • If I save the URL and try to either read or write to it later, the call to startAccessingSecurityScopedResource returns NO. This works if I use bookmarks.
  • If I try bookmarkDataWithOptions:, I get back Error Domain=NSCocoaErrorDomain Code=260 "The operation couldn’t be completed. (Cocoa error 260.)". This works if I create the bookmarks inside the security scope.

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?


Solution

  • 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];
      }
    }