UIDocumentPickerViewController返回一个不存在的文件的URL。

26
我使用 UIDocumentPickerViewController,让用户从iCloud Drive中选择文件进行上传到后端。大部分时间它都是正常的。然而,有时(特别当网络连接不稳定时),documentPicker:didPickDocumentAtURL:会返回一个在文件系统上并不存在的url,任何试图使用该url的尝试都会返回NSError "No such file or directory"。

如何正确处理这个问题? 我考虑使用 NSFileManager fileExistsAtPath:方法,并告诉用户如果路径不存在,则请再次尝试。但是这听起来并不太用户友好。 有没有一种方法可以从iCloud Drive获得真实的错误原因,或者告诉iCloud Drive尝试再次上传?

以下是相关代码:

@IBAction func add(sender: UIBarButtonItem) {
    let documentMenu = UIDocumentMenuViewController(
        documentTypes: [kUTTypeImage as String],
        inMode: .Import)

    documentMenu.delegate = self
    documentMenu.popoverPresentationController?.barButtonItem = sender
    presentViewController(documentMenu, animated: true, completion: nil)
}

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

func documentPicker(controller: UIDocumentPickerViewController, didPickDocumentAtURL url: NSURL) {
    print("original URL", url)

    url.startAccessingSecurityScopedResource()

    var error: NSError?
    NSFileCoordinator().coordinateReadingItemAtURL(
    url, options: .ForUploading, error: &error) { url in
        print("coordinated URL", url)
    }

    if let error = error {
        print(error)
    }

    url.stopAccessingSecurityScopedResource()
}

我通过在OS X上将两个大图像(每个约5MiB)添加到iCloud Drive,并仅在iPhone上打开其中一个文件(a synced file.bmp),而不打开另一个文件(an unsynced file.bmp)来复现此问题。然后关闭了WiFi。然后我尝试在我的应用程序中选择它们:
同步的文件:
original URL file:///private/var/mobile/Containers/Data/Application/CE70EE57-B906-4BF8-B351-A57110BE2B01/tmp/example.com.demo-Inbox/a%20synced%20file.bmp
coordinated URL file:///private/var/mobile/Containers/Data/Application/CE70EE57-B906-4BF8-B351-A57110BE2B01/tmp/CoordinatedZipFileDR7e5I/a%20synced%20file.bmp

未同步的文件:
original URL file:///private/var/mobile/Containers/Data/Application/CE70EE57-B906-4BF8-B351-A57110BE2B01/tmp/example.com.demo-Inbox/an%20unsynced%20file.bmp
Error Domain=NSCocoaErrorDomain Code=260 "The file “an unsynced file.bmp” couldn’t be opened because there is no such file." UserInfo={NSURL=file:///private/var/mobile/Containers/Data/Application/CE70EE57-B906-4BF8-B351-A57110BE2B01/tmp/example.com.demo-Inbox/an%20unsynced%20file.bmp, NSFilePath=/private/var/mobile/Containers/Data/Application/CE70EE57-B906-4BF8-B351-A57110BE2B01/tmp/example.com.demo-Inbox/an unsynced file.bmp, NSUnderlyingError=0x15fee1210 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}

我也遇到了类似的问题,使用UIDocumentPickerViewController从Google Drive导入图像时。返回了一个看起来有效的URL,但是fileExistsAtPath返回nil(但仅偶尔)。我需要使用Import模式(就像你一样),但我注意到如果切换到Open模式,问题似乎会消失。此外,我相信只有在使用Open或Move模式时才需要调用startAccessingSecurityScopedResource。在我的测试中,当使用Import模式时,该调用始终返回false。自发布以来,您是否取得了任何进展? - grfryling
@grfryling 我决定向用户提供一个模糊的错误信息。我尝试了Open模式,发现可以使用"无处不在"函数,如startDownloadingUbiquitousItemAtURL:error:来使用那个不存在的url。但是,我没有使用它,因为Dropbox不支持Open模式。 - imgx64
很棒的老问题。这个问题在书签中也存在,导致书签数据返回为nil并抛出错误代码260。苹果做得好。 - Warpzit
7个回答

44

描述

我遇到了类似的问题。 我的文档选择器是这样初始化的:

var documentPicker: UIDocumentPickerViewController = UIDocumentPickerViewController(documentTypes: ["public.data"], in: .import)
这意味着在从documentPicker选择文件后,文件会被复制到app_id-Inbox目录。当调用委托方法documentPicker(_:didPickDocumentsAt:)时,它会返回指向位于app_id-Inbox目录中的文件的URL。

问题

过了一段时间后(未关闭应用程序),那些URL将指向不存在的文件。这是因为在此期间清除了tmp/文件夹中的app_id-Inbox。例如,我选择文档,在表视图中显示它们,并将iPhone留在该屏幕上大约一分钟,然后当我尝试单击特定文档以使用从documentPicker提供的URL打开文件时,它返回文件不存在。

这似乎是一个错误,因为苹果的文档在此处说明:

UIDocumentPickerModeImport

这些URL引用所选文档的副本。这些文档是临时文件。它们仅在您的应用程序终止之前可用。要保留永久副本,请将这些文件移动到沙盒内的永久位置。

它明确表示“在应用程序终止之前”,但在我的情况下,这是大约一分钟没有打开该URL。

解决方法

将文件从app_id-Inbox文件夹移动到tmp/或任何其他目录,然后使用指向新位置的URL。

Swift 4

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
    let newUrls = 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)
            return tempURL
        } catch {
            print(error.localizedDescription)
            return nil
        }
    }
    // ... do something with URLs
}
尽管系统会处理/tmp目录,但建议在不再需要时清除其内容。

获取数据非常容易。谢谢。 - SThakur
使用这种方法,我的一个问题得到了解决。但还有一个问题存在。那就是:当我选择一个文档并点击我的iPhone的锁定按钮,然后在2分钟后回到应用程序时,它会显示“文件不存在”。@Najdan - Muneeb Rehman
1
@MuneebRehman,你可以尝试将其移动到其他目录以查看是否有助于解决问题。但首先请检查选择的文档是否真的位于临时目录中,以及您是否在使用来自文档选择器而非临时目录中文档的URL。 - Najdan Tomić
嗨@NajdanTomić 这解决了我的问题,我有一个问题,你建议我自己创建文件夹还是让文件留在tmp文件夹中? - Ammar
1
@Ammar 如果只是暂时使用(上传到您的服务器,通过网络发送),请将其保留在临时文件夹中。 - Najdan Tomić
显示剩余8条评论

8

我认为问题并不在于 tmp 目录被某种方式清理了。

如果您使用模拟器并从 iCloud 打开文件,您会发现该文件存在于 app-id-Inbox 中,而且未被清除/删除。因此,当您尝试读取文件或复制文件并出现文件不存在的错误时,我认为这是一个安全问题,因为您可以看到该文件仍然存在。

在 DocumentPickerViewController 的导入模式中,我使用以下代码解决了这个问题(抱歉,我将粘贴 C# 代码而不是 Swift,因为它就在我面前):

在返回 NSUrl 的 DidPickDocument 方法中,我执行了以下操作:

NSData fileData = NSData.FromUrl(url);

现在您有一个名为 "fileData" 的变量,其中包含文件数据。

然后,您可以将它们复制到应用程序的隔离空间中的自定义文件夹中,这样就能正常工作了。


似乎是一个安全问题。我还不确定自己做错了什么,但是当使用FileManager检查文件时,该文件不存在,然而当我尝试读取它时,出现以下错误:上传准备就绪,索赔89A76A7F-C027-4DFA-B01A-7AEC004F6A21完成时出现错误:Error Domain=NSCocoaErrorDomain Code=257 "The file “test.pdf” couldn’t be opened because you don’t have permission to view it." - zumzum
在选择器返回的fileURL上调用startAccessingSecurityScopedResource(),然后允许FileManager.default.fileExists(atPath: fileURL.path)为true。确保在平衡调用时调用stopAccessingSecurityScopedResource()。 - zumzum

5
使用文档选择器时,要使用文件系统,请将 URL 转换为以下文件路径:
var filePath = urls[0].absoluteString
filePath = filePath.replacingOccurrences(of: "file:/", with: "")//making url to file path

3
如果你只需获取文件系统路径而不需要 file:// 方案,使用 urls[0].path 更为简单。 - Steve Madsen

2
我遇到了类似的问题。一段时间后,URL路径中的文件被删除了。 我通过使用.open UIDocumentPickerMode来解决了这个问题,而不是使用.import
let importMenu = UIDocumentPickerViewController(documentTypes: [String(kUTTypeData)], in: .open)

importMenu.delegate = self

importMenu.modalPresentationStyle = .formSheet

present(importMenu, animated: true, completion: nil)

在这种情况下,所选文档的URL路径将在下面的委托方法中更改。
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {

    print("url documentPicker:",urls)

}

现在您可以观察到路径已更改。现在我们获取的是文件所在的确切路径。因此,它不会在一段时间后被删除。对于iPad和模拟器,文件存储在“文件提供程序存储”文件夹下,而对于iPhone,文件将存储在“文档”文件夹下。使用此路径,您还可以获得文件的扩展名和名称。

1

我曾经遇到同样的问题,后来发现是在视图出现时删除了Temp目录。因此,在出现并正确调用documentPicker:didPickDocumentAtURL:时,它会保存然后删除,只有url指向我已删除的文件。


天啊...我为这个问题苦苦挣扎了几个小时,结果原来是这个原因!视图控制器在应用程序的完全不同部分,在viewDidAppear时清理了tmp目录,彻底删除了文档选择器中刚创建的文件。这真是个巧合,很难追踪)) - IPv6
太好了,我不是唯一一个遇到这个问题的人!很高兴它有帮助! - Jesper

1

我可以为这个问题添加一些有趣的内容。

当重复导入同一文件时,也会出现此问题。我曾在本地文件系统上遇到过非常小的文件(〜900字节)也出现了这个问题。

iOS将在将文件放入收件箱并通知委托60秒后精确删除该文件。如果您使用相同名称(或最后路径组件)再次导入文件,则似乎这些删除操作可能会排队并在60秒后开始造成破坏。这种情况发生在iOS 13(弃用初始化程序)和14(新初始化程序)上。

我尝试了一个经过充分测试的代码片段来执行协调删除,但没有帮助。

这个问题仍然非常严重,并且对导入功能是很大的阻碍;我认为使用情况并不是非常罕见或异想天开的。虽然关于此问题的帖子不多 - 也许每个人都切换到从导入打开(即使这需要协调文件操作,如果您想成为好公民)?


1

以下是使用 Swift 5 编写的完整代码,支持早期版本的 iOS 14 及以后版本

此方法在 iOS 14 中已弃用

public init(documentTypes allowedUTIs: [String], in mode: UIDocumentPickerMode)
  1. Write this code in your button action

    @IBAction func importItemFromFiles(sender: UIBarButtonItem) {
    
             var documentPicker: UIDocumentPickerViewController!
             if #available(iOS 14, *) {
                 // iOS 14 & later
                 let supportedTypes: [UTType] = [UTType.image]
                 documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes)
             } else {
                 // iOS 13 or older code
                 let supportedTypes: [String] = [kUTTypeImage as String]
                 documentPicker = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
             }
             documentPicker.delegate = self
             documentPicker.allowsMultipleSelection = true
             documentPicker.modalPresentationStyle = .formSheet
             self.present(documentPicker, animated: true)
         }
    
  2. Implement Delegates

// MARK: - UIDocumentPickerDelegate Methods

extension MyViewController: UIDocumentPickerDelegate {
   func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {

    for url in urls {
        
        // Start accessing a security-scoped resource.
        guard url.startAccessingSecurityScopedResource() else {
            // Handle the failure here.
            return
        }
        
        do {
            let data = try Data.init(contentsOf: url)
            // You will have data of the selected file
        }
        catch {
            print(error.localizedDescription)
        }
        
        // Make sure you release the security-scoped resource when you finish.
        defer { url.stopAccessingSecurityScopedResource() }
    }        
  }

   func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
      controller.dismiss(animated: true, completion: nil)
  }
}

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