Swift文件实际存在却报‘文件不存在’错误的技术求助
解决SwiftUI文件选择后复制提示“文件不存在”的问题
问题核心原因
你遇到的问题本质是安全作用域资源的访问生命周期错误:对File Provider(如iCloud、第三方文件应用)中的文件进行复制时,权限被提前收回,导致系统无法访问文件内容。
看你的代码流程:
- 调用
startAccessingSecurityScopedResource()获取文件权限 - 调用
manageFilePicked执行复制操作 - 复制操作尚未完成(即使是同步方法,File Provider文件可能涉及后台内容加载),就立刻调用
stopAccessingSecurityScopedResource()收回权限
虽然manageFilePicked是同步方法,但对于远程/云存储的File Provider文件,copyItem(at:to:)底层依赖持续的安全作用域权限才能完成内容加载与复制,提前收回权限会触发系统判定“文件不存在”。
另外,你在获取权限前就将URL存入observedModel.selectedFile,虽然不会直接报错,但不符合安全作用域的最佳实践。
解决方案
1. 调整安全作用域权限的覆盖范围
将stopAccessingSecurityScopedResource()的调用延迟到所有文件操作(复制、分析)完成之后,同时确保错误路径下也能正确释放权限:
修改manageSelectedFile方法
private func manageSelectedFile(url: URL) { print("1.AnalyzeFileView::manageSelectedFile.url=[\(url)]") guard url.startAccessingSecurityScopedResource() else { manageError(NSError(domain: "YourAppDomain", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法获取文件访问权限"])) return } print("2.AnalyzeFileView::manageSelectedFile.url=[\(url)]") observedModel.selectedFile = url // 通过do-catch+finally确保权限一定会被释放 do { try observedModel.presenter?.manageFilePicked(url: url) // 如果文件分析是同步操作,需放在此处,确保权限覆盖整个流程 } catch { manageError(error) } finally { url.stopAccessingSecurityScopedResource() } }
修改manageFilePicked方法,使其支持抛出错误
func manageFilePicked(url: URL) throws { let fileManager = FileManager.default let directoryPaths = fileManager.urls(for: .documentDirectory, in: .userDomainMask) guard let publicDocumentsDir = directoryPaths.first else { throw NSError(domain: "YourAppDomain", code: -2, userInfo: [NSLocalizedDescriptionKey: "无法获取文档目录"]) } print("3.manageFilePicked.pathToFilePicked=[\(url.absoluteString)]") let destinationURL = publicDocumentsDir.appendingPathComponent("temporaryfile.\(url.pathExtension)") print("4.manageFilePicked.destinationURL=[\(destinationURL)]") if fileManager.fileExists(atPath: destinationURL.path) { print("4.1.manageFilePicked.trying to remove item at=[\(destinationURL.path)]") try fileManager.removeItem(atPath: destinationURL.path) } print("5.manageFilePicked.trying to copy item from [\(url)] to [\(destinationURL)]") try fileManager.copyItem(at: url, to: destinationURL) self.pathFromFileBrowser = destinationURL.path print("5.1.manageFilePicked.pathFromFileBrowser=[\(pathFromFileBrowser ?? "nil")]") // 同步文件分析操作放在此处 }
2. 异步分析场景的适配
如果文件分析是异步操作,不能在finally中立刻释放权限,需通过回调延迟释放:
修改manageFilePicked为异步回调模式
func manageFilePicked(url: URL, completion: @escaping (Result<String, Error>) -> Void) { let fileManager = FileManager.default let directoryPaths = fileManager.urls(for: .documentDirectory, in: .userDomainMask) guard let publicDocumentsDir = directoryPaths.first else { completion(.failure(NSError(domain: "YourAppDomain", code: -2, userInfo: [NSLocalizedDescriptionKey: "无法获取文档目录"]))) return } let destinationURL = publicDocumentsDir.appendingPathComponent("temporaryfile.\(url.pathExtension)") DispatchQueue.global(qos: .utility).async { do { if fileManager.fileExists(atPath: destinationURL.path) { try fileManager.removeItem(atPath: destinationURL.path) } try fileManager.copyItem(at: url, to: destinationURL) self.pathFromFileBrowser = destinationURL.path // 执行异步分析操作 self.analyzeFile(at: destinationURL) { _ in completion(.success(destinationURL.path)) } } catch { completion(.failure(error)) } } }
修改manageSelectedFile处理异步回调
private func manageSelectedFile(url: URL) { print("1.AnalyzeFileView::manageSelectedFile.url=[\(url)]") guard url.startAccessingSecurityScopedResource() else { manageError(NSError(domain: "YourAppDomain", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法获取文件访问权限"])) return } print("2.AnalyzeFileView::manageSelectedFile.url=[\(url)]") observedModel.selectedFile = url observedModel.presenter?.manageFilePicked(url: url) { result in defer { url.stopAccessingSecurityScopedResource() } switch result { case .success(let path): // 处理成功结果 case .failure(let error): self.manageError(error) } } }
额外注意事项
- 所有对安全作用域URL的操作必须在
startAccessingSecurityScopedResource()和stopAccessingSecurityScopedResource()之间完成,包括读取、复制、移动等操作。 - 对于File Provider文件,建议将复制和分析操作放在后台队列执行,避免阻塞主线程。
- 必须处理
startAccessingSecurityScopedResource()返回false的情况,及时向用户提示权限获取失败。
内容的提问来源于stack exchange,提问作者Wonton
相关产品推荐
相关产品推荐

