如何确保UIDocumentPickerController选中文件夹内文件完成下载再压缩?
这个问题绝对是iCloud Drive文件处理里的经典坑——那些.icloud占位符文件看起来像正常文件,但实际只是指向云端的指针,直接压缩的话只会得到空的zip包。我来分享一套经过验证的方案,结合NSFileCoordinator和iCloud的下载API,确保所有文件完全落地后再执行压缩:
核心思路
必须先检查每个iCloud文件的下载状态,对未下载的文件触发云端下载,等待所有文件完成后再启动压缩流程。全程要通过NSFileCoordinator进行操作,避免iCloud文件的并发访问冲突。
1. 单个文件的下载检查与触发
拿到UIDocumentPickerController返回的URL后,先通过文件协调器获取安全的访问URL,再判断文件的iCloud状态:
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let targetURL = urls.first else { return } let fileCoordinator = NSFileCoordinator(filePresenter: nil) var coordinatorError: NSError? // 协调文件访问,确保操作安全 fileCoordinator.coordinate(readingItemAt: targetURL, options: .withoutChanges, error: &coordinatorError) { coordinatedURL in let fileManager = FileManager.default // 跳过非iCloud文件 guard fileManager.isUbiquitousItem(at: coordinatedURL) else { self.compressFile(at: coordinatedURL) return } do { // 获取文件的下载状态 let downloadStatus = try fileManager.ubiquitousItemDownloadingStatus(for: coordinatedURL) switch downloadStatus { case .current, .downloaded: // 文件已在本地,直接压缩 self.compressFile(at: coordinatedURL) case .notDownloaded: // 触发云端下载,等待完成后再压缩 fileManager.startDownloadingUbiquitousItem(at: coordinatedURL) { downloadError in if let downloadError = downloadError { print("下载失败:\(downloadError.localizedDescription)") // 这里可以给用户弹出错误提示 return } self.compressFile(at: coordinatedURL) } @unknown default: fatalError("遇到未知的iCloud文件下载状态") } } catch { print("检查文件状态失败:\(error.localizedDescription)") } } if let error = coordinatorError { print("文件协调失败:\(error.localizedDescription)") } }
2. 文件夹层级的递归处理
如果用户选中的是文件夹,需要递归遍历所有子文件/子文件夹,确保每一个iCloud文件都完成下载:
// 递归下载文件夹内所有内容 func downloadAllItems(in folderURL: URL, completion: @escaping (Error?) -> Void) { let fileCoordinator = NSFileCoordinator(filePresenter: nil) let dispatchGroup = DispatchGroup() var encounteredError: Error? fileCoordinator.coordinate(readingItemAt: folderURL, options: .withoutChanges) { coordinatedFolderURL in do { // 获取文件夹内所有内容,同时请求iCloud相关属性 let contents = try FileManager.default.contentsOfDirectory( at: coordinatedFolderURL, includingPropertiesForKeys: [.isUbiquitousItemKey, .ubiquitousItemDownloadingStatusKey], options: .skipsHiddenFiles ) for itemURL in contents { dispatchGroup.enter() // 判断是否为文件夹,递归处理 var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory), isDirectory.boolValue { self.downloadAllItems(in: itemURL) { error in encounteredError = error ?? encounteredError dispatchGroup.leave() } } else { // 处理单个文件 self.downloadSingleItem(at: itemURL) { error in encounteredError = error ?? encounteredError dispatchGroup.leave() } } } } catch { encounteredError = error // 手动触发leave,避免group永远等待 dispatchGroup.leave() } } // 等待所有下载任务完成 dispatchGroup.notify(queue: .main) { completion(encounteredError) } } // 单个文件的下载逻辑封装 func downloadSingleItem(at url: URL, completion: @escaping (Error?) -> Void) { let fileCoordinator = NSFileCoordinator(filePresenter: nil) fileCoordinator.coordinate(readingItemAt: url, options: .withoutChanges) { coordinatedURL in let fileManager = FileManager.default guard fileManager.isUbiquitousItem(at: coordinatedURL) else { completion(nil) return } do { let downloadStatus = try fileManager.ubiquitousItemDownloadingStatus(for: coordinatedURL) switch downloadStatus { case .current, .downloaded: completion(nil) case .notDownloaded: fileManager.startDownloadingUbiquitousItem(at: coordinatedURL, completionHandler: completion) @unknown default: completion(NSError(domain: "YourAppDomain", code: -1, userInfo: [NSLocalizedDescriptionKey: "未知的下载状态"])) } } catch { completion(error) } } }
然后在documentPicker中调用这个递归方法,等全部下载完成后再压缩:
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let targetURL = urls.first else { return } var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: targetURL.path, isDirectory: &isDirectory), isDirectory.boolValue { // 处理文件夹 downloadAllItems(in: targetURL) { error in if let error = error { print("文件夹内容下载失败:\(error.localizedDescription)") return } self.compressFolder(at: targetURL) } } else { // 处理单个文件 downloadSingleItem(at: targetURL) { error in if let error = error { print("文件下载失败:\(error.localizedDescription)") return } self.compressFile(at: targetURL) } } }
3. 关键注意事项
- 用户体验:大文件夹下载可能耗时很久,一定要给用户显示进度提示(比如用
Progress类配合UIProgressView),避免用户误以为APP卡顿。 - 取消逻辑:如果用户中途取消操作,记得调用
FileManager.default.cancelDownloadingUbiquitousItem(at:)终止下载,节省带宽和存储空间。 - 空间检查:下载前最好检查本地剩余存储空间,避免因空间不足导致下载失败。
- 后台支持:如果需要在后台继续下载,要在
Info.plist中添加UIBackgroundModes的fetch和remote-notification权限,同时配置iCloud的后台下载策略。
内容的提问来源于stack exchange,提问作者DrMickeyLauer
相关产品推荐
相关产品推荐

