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.
Version
- Swift 3, iOS 10, Xcode 8

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:
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.
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.
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.
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.
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.
ExportToService
In the export operation, the data is duplicated from the third-party app to your app’s container.
MoveToService
In the move operation, the third-party app moves a file from its container to your app’s container.
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:
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.
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.
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.
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:
- 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. - Create the directory, if it does not already exist on the file system.
- 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
andUIDocumentModeOpen
tasks require the user to choose a file the app has made available for the task. -
Provide a file. Both the
UIDocumentModeExportToService
andUIDocumentModeMoveToService
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:
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.
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:
- Checks whether an
originalURL
is passed, whether the URL has a validpathExtension
and whether thepathExtension
matches what is configured in theNote.fileExtension
property. If it does not match, it shows the warning label. - 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:
- Only execute the following code for the
ExportToService
andMoveToService
options. - Use the
fileCoordinator
to read the file to be exported or moved. - Copy the item at the
sourceURL
to thedestURL
. - Call
dismissGrantingAccessToURL(_:)
and pass in thedestURL
. 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 (forMoveToService
) or keep its copy (forExportToService
).
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.
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:
- Call the method whenever a shared file is accessed. It is responsible for providing access to the file on disk.
- Verify that the content of the URL is valid; if not, call the
completionHandler
. - 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!
Comments