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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iOS Extensions: Document Provider Tutorial
25 mins
- Getting Started
- Architecture of CleverNote’s Data Storage
- Where CleverNote’s Files Are Currently Stored
- Allowing Data to Be Shared Between Apps
- Providing Files to Other Developers’ Apps
- Adding Your Document Provider Extension
- Setting Up App Groups
- Storing to the Shared Container
- Creating the Document Picker Experience
- Import/Open Experience
- Export/Move Experience
- Hooking up the File Provider
- Where to Go From Here?
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!