iOS Extensions: Document Provider Tutorial

In this Document Provider tutorial, you’ll learn how to create a UIDocumentProvider extension that allows other apps to interact with your app’s documents. By Dave Krawczyk.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Export/Move Experience

The reason the file list shows up inside the Pages app is that Pages is looking to import a file into its own list of files. For Export and Move, you don’t want to offer a list of files, but instead confirm that your app will accept whatever file is being provided. (Currently, your app only supports files with a .txt extension.)

To set this up, open MainInterface.storyboard under the Picker and add a UIView covering the entire UITableView. Set the view’s Background Color to Dark Gray Color.

Add a UIButton inside the view, centering the button horizontally and vertically. Set the title to Confirm.

Add a UILabel that says CleverNote only accepts .txt files!. Change the textColor to red, and center it horizontally and vertically inside the parent view.

Connect these three elements to the DocumentPickerViewController as IBOutlets, naming them as follows:

@IBOutlet weak var confirmButton: UIButton!
@IBOutlet weak var confirmView: UIView!
@IBOutlet weak var extensionWarningLabel: UILabel!

DocumentPickerViewController allows you to override the method prepareForPresentationInMode(_:), so any decisions about the user interface display will be made here. Replace the prepareForPresentationInMode(_:) method with the code below:

override func prepareForPresentation(in mode: UIDocumentPickerMode) {

  // If the source URL does not have a path extension supported
  // show the extension warning label. Should only apply in
  // Export and Move services
  if let sourceURL = originalURL,
    sourceURL.pathExtension != Note.fileExtension {
      confirmButton.isHidden = true
      extensionWarningLabel.isHidden = false
  }

  switch mode {
  case .exportToService:
    //Show confirmation button
    confirmButton.setTitle("Export to CleverNote", for: UIControlState())
  case .moveToService:
    //Show confirmation button
    confirmButton.setTitle("Move to CleverNote", for: UIControlState())
  case .open:
    //Show file list
    confirmView.isHidden = true
  case .import:
    //Show file list
    confirmView.isHidden = true
  }
}

Let’s go over this step by step. This code:

  1. Checks whether an originalURL is passed, whether the URL has a valid pathExtension and whether the pathExtension matches what is configured in the Note.fileExtension property. If it does not match, it shows the warning label.
  2. Decides whether to display the file listing or the button. If displaying the button, it also configures the correct title based on the UIDocumentPickerMode.

When you copy the file you’ll want to use the NSFileCoordinator, since this notifies any other entity displaying the file of changes that occur. Open DocumentPickerViewController.swift, then add the following declaration below the notes variable:

lazy var fileCoordinator: NSFileCoordinator = {
  let fileCoordinator = NSFileCoordinator()
  fileCoordinator.purposeIdentifier = self.providerIdentifier
  return fileCoordinator
}()

Finally, open MainInterface.storyboard and connect your button’s TouchUpInside action to this method:

// MARK: - IBActions
extension DocumentPickerViewController {

  @IBAction func confirmButtonTapped(_ sender: AnyObject) {
    guard let sourceURL = originalURL else {
      return
    }
    
    switch documentPickerMode {
    case .moveToService, .exportToService:
      let fileName = sourceURL.deletingPathExtension().lastPathComponent
      guard let destinationURL = Note.fileUrlForDocumentNamed(fileName) else {
          return
      }

      fileCoordinator.coordinate(readingItemAt: sourceURL, options: .withoutChanges, error: nil, byAccessor: { [weak self] newURL in
        do {
          try FileManager.default.copyItem(at: sourceURL, to: destinationURL)
          self?.dismissGrantingAccess(to: destinationURL)
        } catch _ {
          print("error copying file")
        }
      })

    default:
      dismiss(animated: true, completion: nil)
    }
  }
}

Let’s go over this step by step. Here you:

  1. Only execute the following code for the ExportToService and MoveToService options.
  2. Use the fileCoordinator to read the file to be exported or moved.
  3. Copy the item at the sourceURL to the destURL.
  4. Call dismissGrantingAccessToURL(_:) and pass in the destURL. This will give the third-party app the new URL. At this point the third-party app can either delete its copy of the file (for MoveToService) or keep its copy (for ExportToService).

Build and run. Again, there’s no visible difference in your app. Go to the Pages app and tap the action button next to the + button in the top left corner. Select Move to… and choose a file to move. Tap Locations in the top left, and then select Picker. You should see the app’s warning label, because the app does not accept files with .pages extensions.

document provider tutorial

Hooking up the File Provider

Now for the grand reveal! It’s time to make this all work by hooking up the File Provider extension. By now you have realized that any time you attempt to load the contents of a file, they won’t load.

To fix that, open FileProvider.swift and navigate to the method startProvidingItemAtURL(_:completionHandler:). Replace that method with the following:

// 1
override func startProvidingItem(at url: URL, completionHandler: @escaping (Error?) -> ()) {
  guard let fileData = try? Data(contentsOf: url) else {
    // NOTE: you would generate an NSError to supply to the completionHandler
    // here however that is outside of the scope for this tutorial
    completionHandler(nil)
    return
  }

  do {
    _ = try fileData.write(to: url, options: NSData.WritingOptions())
    completionHandler(nil)
  } catch let error as NSError {
    print("error writing file to URL")
    completionHandler(error)
  }
}

With this code you:

  1. Call the method whenever a shared file is accessed. It is responsible for providing access to the file on disk.
  2. Verify that the content of the URL is valid; if not, call the completionHandler.
  3. Since you’re not manipulating the file data, write it back to the file, then call the completion handler.

Build and run. Success! Of course, there’s no visible difference in your app, so below are some ways to test this new functionality:

  • Create and open notes in the CleverNote app.
  • Use the Google Drive app to upload CleverNote files by launching Drive, hitting the + button and then hitting Upload.
  • Import notes into the Pages app.
  • Use any app that uses the Document Picker.

Where to Go From Here?

Congratulations on completing this Document Provider tutorial! You’re now able to build a whole new line of apps that provide documents to other apps. They play well with other apps, share nicely and won’t get sent to time-out. :]

Download the completed project here.

Consider taking a deeper dive into Building a Document-Based App with this WWDC video, or check out Ray’s tutorial, which goes beyond the basics for apps using UIDocument class and iCloud. You should also check out this great video overview of App Extensions.

Have you discovered any apps that use the Document Picker or feature a Document Provider extension? Feel free to continue the discussion in the comments below!

Dave Krawczyk

Contributors

Dave Krawczyk

Author

Over 300 content creators. Join our team.