如何从我的iOS应用程序中访问iCloud Drive中的文件?

21

是否有一种类似于UIImagePickerController()的方式从iCloud Drive选择文件的方法?

9个回答

29

你可以这样呈现控制器:

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)

在您的代理中实现该方法:

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

请注意,使用UIDocumentPickerViewController不需要设置iCloud权限。苹果提供了演示如何使用此控制器的示例代码此处


我能否自定义UI以替换UIDocumentPickerViewController? - Yu-Sen Han
3
请确保导入MobileCoreServices。 - Umar Farooque
UIDocumentPickerViewController是否支持选择多个文件? - grep
不支持 iOS 14。请查看下面的答案以获取更新。 - fabian789

7

Swift 5, iOS 13

Jhonattan和Ashu的答案肯定是核心功能的正确方向,但在多文档选择、错误结果和废弃的文档选择API方面存在一些问题。

以下代码展示了一个常见用例的现代实现方式:选择外部iCloud文档以导入到应用程序并进行操作

请注意,您必须设置应用程序的能力以使用iCloud文档,并在应用程序的.plist中设置普及容器...例如,请参见: Swift将文档文件写入/保存/移动到iCloud驱动器

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)
        }
    }

}

这个答案是可行的,但我做了两个小调整。在 didPiskDocumentsAt urls... 方法中,我使用了 guard let firstURL = urls.first else { return } 替代了 let firstURL = urls[0]。在同一个方法中稍微往下一点,有一个对 navigationController?.dismiss(...) 的调用。我不得不将这一行注释掉,因为 DocumentPicker 所在的 vc 本身是以模态方式呈现的,这个 dismiss 调用会将整个 vc 和 Document Picker 一起关闭。除此之外,这个答案非常好用!谢谢 :) - Lance Samaria
1
啊,是的,这是我最喜欢的两个“苹果主义”:编写API时必须“保护”访问必须存在的目录,以及基于演示上下文的视图控制器的所有无数“让其消失”的替代解除机制。哈!苹果的API可以改进一些,你的编辑是100%正确的。 - Bill Patterson

5
这在iOS 14中再次改变了!!
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)
}

4
文档选择器在用户选择应用沙盒之外的目标位置后,调用委托的documentPicker: didPickDocumentAtURL:方法。系统将您的文档副本保存到指定的目标位置。文档选择器提供副本的URL以表示成功;但是,您的应用程序无法访问此URL所引用的文件。 链接这段代码对我有效:
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)
    }

2
让isSecuredURL等于url.startAccessingSecurityScopedResource()是否为真 - 这对我的iCloud Drive链接问题非常有帮助,已经解决了。 - Vlad E. Borovtsov
以这种方式,您只能在第一个文件(url)上以协调的方式执行与读取相关的操作。如果UIDocumentPickerViewControllerallowsMultipleSelection为true呢? - Giorgio

4

Swift 4.X

您需要在XCode功能中启用iCloud授权。此外,您还必须在Apple的开发者帐户中打开您的应用程序包中的iCloud。一旦完成这些步骤,您就可以按照以下方式呈现文档选择器控制器:

使用UIDocumentPickerDelegate方法。

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)    
    }
}

在按钮操作中添加以下代码。
@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)

    }

1
另外,UIDocumentMenuDelegate已被弃用,您应该改用UIDocumentPickerViewController - Sylvan D Ash
3
似乎无法处理未同步的文件。我在返回的文件中得到了 .icloud 文件。我该如何确保这些文件已同步? - vomi

1
对于我的SwiftUI用户来说:这很容易。
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)")
            }
    }
}

1
在这个例子中,使用一个标准的按钮不是更好吗?谢谢! - Diego Freniche
是的,抱歉!这是我代码库中的一部分。我会更新帖子。 - Bas9990

1
iCloudUrl.startAccessingSecurityScopedResource() 

此时,// 对我返回了 true。

然而,以下代码出现了错误:

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

由于卷是只读的,您无法保存文件“xyz”。

这个实际上可以工作:

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

这是有道理的,因为URL可能携带着它的安全访问权限,但这个小疏忽让我困惑了半天...


0

我使用

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

使用copyItem而不是moveItem。否则,文件将从iCloud Drive中删除,这不是我想要的。


0

不需要移动/删除文件或者进行其他操作,如果我们按照相关步骤,苹果提供了一个开箱即用的解决方案,下面是一个代码片段,可以打开iCloud的URL。

这个解决方案涉及以下内容:

  • 开始访问安全范围内的资源。
  • 使用文件协调来安全访问iCloud文件。
  • 详细处理错误以有效诊断问题。

代码片段:

func handleICloudFile(at url: URL) -> ConcreteFile? {
    guard url.startAccessingSecurityScopedResource() else {
        print("Failed to access security-scoped resource.")
        return nil
    }

    defer { url.stopAccessingSecurityScopedResource() }

    var error: NSError?
    var concreteFile: ConcreteFile?

    NSFileCoordinator().coordinate(readingItemAt: url, options: [], error: &error) { (newURL) in
        do {
            let data = try Data(contentsOf: newURL)
        } catch  { 
            print("Error: \(error.localizedDescription)")
        }
    }

    if let error = error {
        print("File coordination error: \(error)")
    }
 return concreteFile
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接