iOS Extensions: Document Provider Tutorial

Dave Krawczyk
document provider tutorial

Allow this Document Provider tutorial to shed some light on this often overlooked iOS extension.

Note: This tutorial has been updated (23/09/2016) to use Xcode 8, iOS 10, and Swift 3.

Kids on the playground are told to share and play nice, or else they get put in time-out; these days, with users craving seamless interactivity, apps have to do the same (or they’ll get deleted). Users don’t want to leave one app to take advantage of another app’s capabilities or access another app’s files, so how can you extend the functionality of your app to be used by other apps on a device? The answer is in this Document Provider tutorial: iOS extensions and the Document Provider extension.

The Document Provider extension (commonly referred to as the document provider) allows an app to share its documents with the other apps on a user’s device in a safe and convenient way. If you have ever used the Document Picker, you might have admired all the apps in the Locations section. Maybe you wondered: how do I get my app to show up there?

Today, young grasshopper, you’ll find out. :]

In this Document Provider tutorial, you’ll be working on an app called CleverNote. CleverNote starts out as just an app that allows a user to create and edit simple text notes. You’ll be adding the document provider, which will allow users to do the following:

  • Open and edit CleverNote notes in other apps.
  • Move notes into the CleverNote app.
  • Import notes from other apps into CleverNote.
  • Export notes from CleverNote to other apps.

These documents aren’t going to provide themselves, so let’s get started!

Getting Started

Download the starter project here. Build and run (Product \ Run or ⌘R) the project in Xcode, and you’ll see the following:

document provider tutorial

Tapping on the + button in the upper right allows you to create a new note. After you add a title and text, hitting the save button will save the note to your device’s document storage directory. That’s great … except it can’t be accessed by other devices, which means your app exists in its own little closed world, and your user is getting annoyed. But don’t worry—you’re about to change all of that. ;]

Architecture of CleverNote’s Data Storage

In order to best understand how to share data, it is important to have a solid understanding of the iOS file system.

Where CleverNote’s Files Are Currently Stored

At this point, the notes created in the CleverNote app are stored in the Documents Directory. This is the directory where you can store user-generated content.

Note: When using the iOS Simulator, you can open the directory that contains these user-generated files. This is very helpful when debugging. To do this, open Note.swift and uncomment the print inside the localDocumentsDirectoryURL() method, then run the app. You should see something like this print in the Console:

file:///Users/davekrawczyk/Library/Developer/CoreSimulator/Devices/91D24DEF-BE5E-4EA1-816A-F5CC1201942A/data/Containers/Data/Application/335CBBC8-7A41-4ACD-925F-1E8D1CB83640/Documents/

That is the URL to the iPhone Simulators Documents directory. Copy that URL (excluding the file://prefix), open Finder on your Mac, hit CMD + SHIFT + G to jump to a folder, and paste the URL. Any created notes should show up right there. To learn more about the different standard iOS Directories, check this out.

Currently, the notes you’re creating are not visible to other apps because of the idea of an app’s sandbox.

document provider tutorial

You can think of the sandbox as a safe area for the app to play. An app should never be playing, or functioning, outside of its designated sandbox. In the above diagram, CleverNote sits on top of its own sandbox, but no other apps are allowed to play in CleverNote’s sandbox. Even an app extension does not have access to anything in the app’s sandbox.

Allowing Data to Be Shared Between Apps

iOS 8 brought us the concept of an app group: a collection of apps created by the same development team that allows two or more apps to access the shared data containers.

document provider tutorial

This concept allows apps or extensions to have a shared set of files. Awesome, right? But only if your user only uses your apps. What about helping your app play nice with apps from different designers?

Providing Files to Other Developers’ Apps

If your app needs to share its files with apps that are not part of your app group, this can be done with a UIDocumentProvider extension that has access to your app group. This extension allows other developers’ apps to access your files using the UIDocumentPicker, as seen below.

document provider tutorial

UIDocumentProvider provides four modes of interaction when accessing files:

Import
In the import operation, the data is duplicated from your app’s container to the third-party app.
document provider tutorial

Open
In the open operation, the third-party app is able to modify a file from your app’s container and then save it back to your app’s container.
document provider tutorial

ExportToService
In the export operation, the data is duplicated from the third-party app to your app’s container.
document provider tutorial

MoveToService
In the move operation, the third-party app moves a file from its container to your app’s container.
document provider tutorial

Now that you know how the data is currently stored and the operations other apps might want to perform on your data, it’s time to start providing some documents!

Adding Your Document Provider Extension

To add the Document Provider extension, first select the CleverNote project in the Project Navigator and click the + button to add a target (the + button is at the very bottom of the Targets list, next to the filter). Select iOS/Application Extension/Document Provider and click Next. Type Picker for the Product Name and make sure to check the box to Include a File Provider Extension; this will be important later. If Xcode asks if you would like to activate the scheme, click Activate. This will allow you to build and debug the extension.

At this point, your targets should look like the image below:

document provider tutorial

Let’s look through the Project Navigator to see what these targets have added to the project.

Under the Picker directory, you’ll see that Xcode has generated a DocumentPickerViewController. This is a subclass of UIDocumentPickerExtensionViewController, displayed inside a third-party application when using the Document Picker extension.

Under the PickerFileProvider directory, you’ll see the FileProvider class Xcode generated. The FileProvider coordinates all of the reading and writing during the file operations.

Setting Up App Groups

Xcode is very nice and has already started setting up app groups for you.

document provider tutorial

Navigate to your project settings Capabilities pane and select the PickerFileProvider target. The App Groups capability should have an identifier selected. Fix the issues for that target, then do the same for the Picker target. Finally, in the CleverNote target, enable app groups and select the identifier that was selected for the other two targets.

Note: In order to change the App Group information, you must have admin access for the Apple Developer Account. If you do not have this type of access, you will receive a cryptic message like Apple Connection Failed. You must have permissions to change App Group settings for this to work correctly.

Next, make sure the CleverNote build target is selected, then build and run on your device. Using the Pages app, tap the Locations button on the left of the navigation bar. This will bring up a list of apps that have Document Provider extensions.

pages

Note: If you receive an error when trying to load the extension, you might have to turn the device you’re using off and on again. This is due to an Apple bug that causes registration to fail on devices such as the iPhone 6s

Next, select the row titled More and switch on the Picker extension from the list of available providers. Tap Done to exit the menu, then select your extension. If you see a screen similar to the one below, you’re on the right track.

document provider tutorial

This is the default interface for a Document Picker. You’ll set up your own custom interface in a few minutes, but for now just copy down your app group identifier, because you’re about to start coding—yay!

Storing to the Shared Container

Now that your app has a document provider extension and your app group is set up, you need to make sure the extension can see your app’s files. To do this, you need to write files to the app group’s shared container instead of the application container (the Documents Directory).

Open Note.swift, then add the following line below the fileExtension constant, replacing YOUR APP GROUP IDENTIFIER with your own app group identifier:

static let appGroupIdentifier = "YOUR APP GROUP IDENTIFIER"

Next, add the following method above the localDocumentsDirectoryURL() method:

static func appGroupContainerURL() -> URL? {
  // 1
  let fileManager = FileManager.default
  guard let groupURL = fileManager
    .containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) else {
      return nil
  }
    
  let storagePathUrl = groupURL.appendingPathComponent("File Provider Storage")
  let storagePath = storagePathUrl.path
  // 2
  if !fileManager.fileExists(atPath: storagePath) {
    do {
      try fileManager.createDirectory(atPath: storagePath,
                                              withIntermediateDirectories: false,
                                              attributes: nil)
    } catch let error {
      print("error creating filepath: \(error)")
      return nil
    }
  }
  // 3
  return storagePathUrl
}

Let’s look at this step by step. With this code, you:

  1. Use NSFileManager‘s containerURL(forSecurityApplicationGroupIdentifier:) to get the URL for the app group’s shared storage container, then append the ‘File Provider Storage’ path component to the location.
  2. Create the directory, if it does not already exist on the file system.
  3. Return the URL to your app group container’s File Provider Storage directory.

Next, replace the calls to localDocumentsDirectoryURL() with appGroupContainerURL() inside the fileUrlForDocumentNamed(_:) and getAllNotesInFileSystem() methods. The app will now create files in the app group’s shared storage container instead of inside the applications container.

Build and run. Since you’ve changed where the app is pulling files from, you won’t see any of the previously created notes. Go ahead and create a note.

You’ll notice that the note’s text isn’t being loaded when you try to open a saved note. This will be resolved once you hook up the File Provider, but first you’ll want to create the Document Picker experience.

Creating the Document Picker Experience

It’s up to you to create the user interface for the DocumentPickerViewController. There are two main categories of operation you need to plan for when thinking about UIDocumentPickerModes:

  • Choosing a file. Both the UIDocumentModeImport and UIDocumentModeOpen tasks require the user to choose a file the app has made available for the task.
  • Provide a file. Both the UIDocumentModeExportToService and UIDocumentModeMoveToService tasks allow other apps to provide a file to your app. For this feature, the user would wish to see either a confirmation button or, if your app has a hierarchical file system, a file directory listing to choose the specific location.

Let’s start with the first category.

Import/Open Experience

Under the Picker directory, open the MainInterface.storyboard and delete the default ‘Untitled.txt’ button on the DocumentPickerViewController scene. Add a UITableView and connect it to the DocumentPickerViewController class as an IBOutlet named tableView.

Next, add a prototype cell and give it the reuse identifier noteCell. Open DocumentPickerViewController.swift and add the following to the top of your class definition:

var notes = [Note]()

When the DocumentPickerViewController loads, the notes array will be populated with notes from the app’s shared container. Conveniently, there is a static method on the Note class that provides this information. To call this method, include the Picker target inside the Note.swift file’s target membership, as shown below:

document provider tutorial

Open DocumentPickerViewController.swift and add the following method below your IBOutlet definitions:

override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)

  notes = Note.getAllNotesInFileSystem()

  tableView.reloadData()
}

The code above populates the notes array and tells the UITableView to reload itself.

Remove the openDocument(_:) method provided by Xcode, since you do not require this method, then add the following UITableViewDataSource extension at the end of the DocumentPickerViewController file:

// MARK: - UITableViewDataSource
extension DocumentPickerViewController: UITableViewDataSource {
  // MARK: - Cell Identifiers
  private enum CellIdentifier: String {
    case noteCell = "noteCell"
  }
  
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return notes.count
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.noteCell.rawValue,
                                             for: indexPath)
    
    let note = notes[indexPath.row]
    cell.textLabel?.text = note.title
    return cell
  }
}

This extension provides a private enumeration holding the tableView‘s cell identifiers and implements the required methods from the UITableViewDataSource protocol.

Finally, add the following UITableViewDelegate extension below the UITableViewDataSource extension:

// MARK: - UITableViewDelegate
extension DocumentPickerViewController: UITableViewDelegate {
  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    let note = notes[indexPath.row]
    dismissGrantingAccess(to: note.fileURL)
  }
}

This extension handles file selection, providing the URL of the note to the dismissGrantingAccessToURL(_:) method that sends the note URL back to the third-party calling app that initiated the Document Picker.

Build and run the app; you shouldn’t see any differences. Open the Pages app again and repeat the process to open the Picker extension for your app. You should now see a list of all files currently in the app’s shared container, in alphabetical order.

document provider tutorial

Select a file from the Picker; you’ll notice that it doesn’t load. Don’t worry—you’ll resolve this once you finally hook up the elusive File Provider!

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

Dave, @Dave_D5, is Co-Founder & Director of iOS at Windy City Lab in Chicago, a company that is heavily focused on enabling software to interact with physical objects. He is currently developing the apps for a few IoT devices. He spent the last year as Lead Instructor at Mobile Makers Academy in Chicago and San Francisco and loved spending every day enabling future iOS Developers.

Dave's obsessions also include cycling, music, and eating. Bourbon is nice, too.

Other Items of Interest

Big Book SaleAll raywenderlich.com iOS 11 books on sale for a limited time!

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

iOS Team

... 73 total!

Android Team

... 20 total!

Unity Team

... 10 total!

Articles Team

... 15 total!

Resident Authors Team

... 18 total!

Podcast Team

... 7 total!

Recruitment Team

... 9 total!