21

Is there a way to choose file from iCloud Drive similar way to UIImagePickerController()?

Bartłomiej Semańczyk
  • 59,234
  • 49
  • 233
  • 358

8 Answers8

29

You can present controller the following way:

import MobileCoreServices

let documentPickerController = UIDocumentPickerViewController(documentTypes: [String(kUTTypePDF), String(kUTTypeImage), String(kUTTypeMovie), String(kUTTypeVideo), String(kUTTypePlainText), String(kUTTypeMP3)], inMode: .Import)
documentPickerController.delegate = self
presentViewController(documentPickerController, animated: true, completion: nil)

In your delegate implement the method:

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

Note that you don't need to set up iCloud Entitlement to use UIDocumentPickerViewController. Apple provides sample code that demonstrates how to use this controller here

jvarela
  • 3,744
  • 1
  • 22
  • 43
Bartłomiej Semańczyk
  • 59,234
  • 49
  • 233
  • 358
7

Swift 5, iOS 13

Jhonattan's and Ashu's answers are definitely on the right track for the core functionality, there are a number of issues with multiple-document-selection, error outcomes and deprecated document picker API.

The code below shows a modern start-to-finish version of a common use case: pick an external iCloud document to import into app and do something with it.

Note that you have to have your app's Capabilities set up to use iCloud documents and have a ubiquity container set up in your app's .plist... See for example: Swift write/save/move a document file to iCloud drive

class ViewController: UIViewController {
    
    @IBAction func askForDocument(_ sender: Any) {
        
        if FileManager.default.url(forUbiquityContainerIdentifier: nil) != nil {

            let iOSPickerUI = UIDocumentPickerViewController(documentTypes: ["public.text"], in: .import)
            iOSPickerUI.delegate = self
            iOSPickerUI.modalPresentationStyle = .formSheet
            
            if let popoverPresentationController = iOSPickerUI.popoverPresentationController {
                popoverPresentationController.sourceView = sender as? UIView
            }
            self.present(iOSPickerUI, animated: true, completion: nil)
        }
    }

    func processImportedFileAt(fileURL: URL) {
        // ...
    }
}

extension ViewController: UIDocumentPickerDelegate, UINavigationControllerDelegate {
    
    func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
        dismiss(animated: true, completion: nil)
    }
    
    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
        if controller.allowsMultipleSelection {
            print("WARNING: controller allows multiple file selection, but coordinate-read code here assumes only one file chosen")
            // If this is intentional, you need to modify the code below to do coordinator.coordinate
            // on MULTIPLE items, not just the first one
            if urls.count > 0 { print("Ignoring all but the first chosen file") }
        }
        
        let firstFileURL = urls[0]
        let isSecuredURL = (firstFileURL.startAccessingSecurityScopedResource() == true)
        
        print("UIDocumentPickerViewController gave url = \(firstFileURL)")

        // Status monitoring for the coordinate block's outcome
        var blockSuccess = false
        var outputFileURL: URL? = nil

        // Execute (synchronously, inline) a block of code that will copy the chosen file
        // using iOS-coordinated read to cooperate on access to a file we do not own:
        let coordinator = NSFileCoordinator()
        var error: NSError? = nil
        coordinator.coordinate(readingItemAt: firstFileURL, options: [], error: &error) { (externalFileURL) -> Void in
                
            // WARNING: use 'externalFileURL in this block, NOT 'firstFileURL' even though they are usually the same.
            // They can be different depending on coordinator .options [] specified!
        
            // Create file URL to temp copy of file we will create:
            var tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
            tempURL.appendPathComponent(externalFileURL.lastPathComponent)
            print("Will attempt to copy file to tempURL = \(tempURL)")
            
            // Attempt copy
            do {
                // If file with same name exists remove it (replace file with new one)
                if FileManager.default.fileExists(atPath: tempURL.path) {
                    print("Deleting existing file at: \(tempURL.path) ")
                    try FileManager.default.removeItem(atPath: tempURL.path)
                }
                
                // Move file from app_id-Inbox to tmp/filename
                print("Attempting move file to: \(tempURL.path) ")
                try FileManager.default.moveItem(atPath: externalFileURL.path, toPath: tempURL.path)
                
                blockSuccess = true
                outputFileURL = tempURL
            }
            catch {
                print("File operation error: " + error.localizedDescription)
                blockSuccess = false
            }
            
        }
        navigationController?.dismiss(animated: true, completion: nil)
        
        if error != nil {
            print("NSFileCoordinator() generated error while preparing, and block was never executed")
            return
        }
        if !blockSuccess {
            print("Block executed but an error was encountered while performing file operations")
            return
        }
        
        print("Output URL : \(String(describing: outputFileURL))")
        
        if (isSecuredURL) {
            firstFileURL.stopAccessingSecurityScopedResource()
        }
        
        if let out = outputFileURL {
            processImportedFileAt(fileURL: out)
        }
    }

}
Bill Patterson
  • 2,495
  • 1
  • 19
  • 20
  • This answer works but I made 2 small adjustments. In `didPiskDocumentsAt urls...` method instead of `let firstURL = urls[0]` I used `guard let firstURL = urls.first else { return }`. And a little further down in the same method there is a call to `navigationController?.dismiss(...). I had to comment this line out because the vc that the DocumentPicker is in, itself (the vc) was presented modally, that call to dismiss dismissed the entire vc along with the Document Picker. Other than that this answers works wonderfully!!! Thanks :) – Lance Samaria Nov 26 '21 at 15:03
  • 1
    Ah, yes, two of my favorite "apple-isms": an API written so you have to 'guard' when accessing a directory that must exist, and all the myriad "make this go away" alternate dismiss mechanisms for view controllers based on presentation context. Ha! The apple API's could use some improvement, and your edits are 100% correct. – Bill Patterson Dec 01 '21 at 16:09
5

This changed once again in iOS 14!!

Working example for JSON:

import UIKit
import MobileCoreServices
import UniformTypeIdentifiers

func selectFiles() {
    let types = UTType.types(tag: "json", 
                             tagClass: UTTagClass.filenameExtension, 
                             conformingTo: nil)
    let documentPickerController = UIDocumentPickerViewController(
            forOpeningContentTypes: types)
    documentPickerController.delegate = self
    self.present(documentPickerController, animated: true, completion: nil)
}
fabian789
  • 8,348
  • 4
  • 45
  • 91
4

The document picker calls the delegate’s documentPicker:didPickDocumentAtURL: method when the user selects a destination outside your app’s sandbox. The system saves a copy of your document to the specified destination. The document picker provides the copy’s URL to indicate success; however, your app does not have access to the file referred to by this URL. Link

This code work for me:

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
        let url = urls[0]
        let isSecuredURL = url.startAccessingSecurityScopedResource() == true
        let coordinator = NSFileCoordinator()
        var error: NSError? = nil
        coordinator.coordinate(readingItemAt: url, options: [], error: &error) { (url) -> Void in
            _ = urls.compactMap { (url: URL) -> URL? in
                // Create file URL to temporary folder
                var tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
                // Apend filename (name+extension) to URL
                tempURL.appendPathComponent(url.lastPathComponent)
                do {
                    // If file with same name exists remove it (replace file with new one)
                    if FileManager.default.fileExists(atPath: tempURL.path) {
                        try FileManager.default.removeItem(atPath: tempURL.path)
                    }
                    // Move file from app_id-Inbox to tmp/filename
                    try FileManager.default.moveItem(atPath: url.path, toPath: tempURL.path)


                    YourFunction(tempURL)
                    return tempURL
                } catch {
                    print(error.localizedDescription)
                    return nil
                }
            }
        }
        if (isSecuredURL) {
            url.stopAccessingSecurityScopedResource()
        }

        navigationController?.dismiss(animated: true, completion: nil)
    }
Jhonattan
  • 372
  • 3
  • 13
  • 2
    `let isSecuredURL = url.startAccessingSecurityScopedResource() == true` - was helpful and fixed my problem with iCloud Drive urls – void Oct 25 '18 at 05:46
  • In this way you perform read-related operations in a coordinated manner only on the first file (url). What if `allowsMultipleSelection` of the `UIDocumentPickerViewController` is true? – Giorgio Feb 27 '20 at 09:32
4

Swift 4.X

You need to enable iCloud entitlements in XCode Capabilities. Also you have to turn on iCloud in you app bundle in developer account of Apple. Once you do this, you are able to present document picker controller by following way:

Use UIDocumentPickerDelegate methods

extension YourViewController : UIDocumentMenuDelegate, UIDocumentPickerDelegate,UINavigationControllerDelegate {

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

    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) {
        print("url = \(url)")
    }
    
    func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
        dismiss(animated: true, completion: nil)    
    }
}

Add below code for Button Action

@IBAction func didPressAttachment(_ sender: UIButton) {
       
        let importMenu = UIDocumentMenuViewController(documentTypes: [String(kUTTypePDF)], in: .import)
        importMenu.delegate = self
        importMenu.modalPresentationStyle = .formSheet
        
        if let popoverPresentationController = importMenu.popoverPresentationController {
            popoverPresentationController.sourceView = sender
            // popoverPresentationController.sourceRect = sender.bounds
        }
         self.present(importMenu, animated: true, completion: nil)

    }
Nimantha
  • 6,405
  • 6
  • 28
  • 69
Ashu
  • 3,373
  • 38
  • 34
  • 1
    Also `UIDocumentMenuDelegate` was deprecated and you should instead use `UIDocumentPickerViewController` – Sylvan D Ash Oct 06 '18 at 22:13
  • 3
    It does not seem to work with unsynced files. I got .icloud files in the returned files. How can I make sure the files are synced ? – vomi Jan 18 '19 at 14:36
1
iCloudUrl.startAccessingSecurityScopedResource() 

// is returning true for me at this point,

However the following code gave the error:

try FileManager.default.createDirectory(atPath: iCloudUrl, withIntermediateDirectories: true, attributes: nil)

"You can’t save the file “xyz” because the volume is read only."

This actually works :

try FileManager.default.createDirectory(at: iCloudUrl, withIntermediateDirectories: true, attributes: nil)

Which makes sense because the URL probably is carrying around it’s security access, but this little oversight stumped me for half a day…

Hiren
  • 12,720
  • 7
  • 52
  • 72
1

For my swiftUI users: It's quite easy.

struct HomeView: View {
    
    @State private var showActionSheet = false
    
    var body: some View {
        Button("Press") {
            showActionSheet = true
        }
         .fileImporter(isPresented: $showActionSheet, allowedContentTypes: [.data]) { (res) in
                print("!!!\(res)")
            }
    }
}
Bas9990
  • 144
  • 6
0

I use

 try FileManager.default.copyItem(at: url, to: destinationUrl)

instead of moveItem. Otherwise, the files are removed from iCloud Drive, which is not what I want.