UIDocumentMenuViewController demystified

Johan Attali bio photo By Johan Attali

For my current project I needed to integrate a similar functionality as the Dropbox upload process (see following screenshot).

That is being able to upload files from different sources using iOS Extensions introduced in iOS 8 as well as combining it with photos and camera access.

Dropbox Upload

While Apple has done a great job documenting iOS Extensions this simple task was not easy to find.

The first thing to integrate is a UIDocumentMenuViewController and conform to the UIDocumentMenuDelegate.

When a user selects one the existing file sharing extensions on his device this calls the documentMenu:didPickDocumentPicker delegate method. So you simply have to present the selected UIDocumentPickerViewController.

extension ChatViewController: UIDocumentMenuDelegate {
    func documentMenuWasCancelled(documentMenu: UIDocumentMenuViewController) {
        documentMenu.dismissViewControllerAnimated(true, completion: nil)
    }

    func documentMenu(documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController) {
        documentPicker.delegate = self
        self.presentViewController(documentPicker, animated: true, completion: nil)
    }
}

Note that the UIDocumentMenuViewController is automatically dismissed by the OS when delegate methods are called (which is not the case for UIImagePickerController).

As you might have guessed you now have to conform to the UIDocumentPickerDelegate which has similar callbacks methods.

The last piece of the puzzle is unfortunately the least documented part. The documentPicker:didPickDocumentAtURL delegate method does return a proper file path once the user has selected the file to share but that URL is sandboxed even for import mode and doing a simple

let fileData = NSData(contentsOfURL: url)

Will simply return you nil. The solution came from this SO answer. You need to use an NSFileCoordinator and tell the url that you'll need access to its security scope

extension ChatViewController: UIDocumentPickerDelegate {
    func documentPickerWasCancelled(controller: UIDocumentPickerViewController) {
      // nothing was picked
    }

    func documentPicker(controller: UIDocumentPickerViewController, didPickDocumentAtURL url: NSURL) {

        url.startAccessingSecurityScopedResource()
        let coordinator = NSFileCoordinator()
        var error:NSError? = nil
        coordinator.coordinateReadingItemAtURL(url, options: [], error: &error) { (url) -> Void in
          // do something with it
          let fileData = NSData(contentsOfURL: url)  
        }
        url.stopAccessingSecurityScopedResource()
    }
}

And only then can you finally access the document NSData.

Finally adding camera and photos to the UIDocumentMenuViewController is an easy task using the addOptionWithTitle:image:order:handler: method.

let documentTypes = [kUTTypePDF as String, kUTTypeJPEG as String, kUTTypePNG as String]
let documentController = UIDocumentMenuViewController(documentTypes: documentTypes, inMode: .Import)
documentController.delegate = self
documentController.addOptionWithTitle("Photos", image: nil, order: .First, handler: onTapPhotosOption)
documentController.addOptionWithTitle("Camera", image: nil, order: .First, handler: onTapCameraOption)
self.presentViewController(documentController, animated: true, completion: nil)

That's it, you can now safely use the power of iOS 8 Extensions to import files in your app.