UIDocument打开操作失败:iCloud离线修改后联网异常问题
我在应用中使用UIDocument管理iCloud Drive存储,通过NSMetadataQuery检测iCloud容器的变更。联网状态下收到更新事件时,UIDocument可正常打开文档;但先断网修改iCloud容器内容,再联网后,更新事件触发但UIDocument打开失败,调试器报错信息如下:
NoteStack[391:7193] [claims] 7F874C6C-2CF8-46C9-9E50-6881D63A0DE6 grantAccessClaim reply is an error: Error Domain=NSFileProviderErrorDomain Code=-1004 "Your device couldn’t connect to the server. There may be a problem with the server or network." UserInfo={NSFilePath=/private/var/mobile/Library/Mobile Documents/iCloudfusionengineeringtechNoteStack/Documents/A69B9860-9668-40FE-9D09-E6ED3ED5B28D/E3C485D5-4D95-4614-971F-70157053D63C/E3C485D5-4D95-4614-971F-70157053D63C.note, NSLocalizedDescription=Your device couldn’t connect to the server. There may be a problem with the server or network., NSUnderlyingError=0x2815c4360 {Error Domain=NSCocoaErrorDomain Code=4355 "Couldn’t access your iCloud account. The iCloud servers might be unreachable or your settings might be incorrect." UserInfo={NSLocalizedDescription=Couldn’t access your iCloud account. The iCloud servers might be unreachable or your settings might be incorrect.}}}
附上相关代码供参考:
class Note: Identifiable, Codable { var id = UUID() var title: String = "" var dateCreated = Date() var text: String = "" } class NoteDocument: UIDocument { var note = Note() override func load(fromContents contents: Any, ofType typeName: String?) throws { if let contents = contents as? Data { if let arr = try? JSONDecoder().decode(Note.self, from: contents) { self.note = arr return } } //if we get here, there was some kind of problem throw NSError(domain: "NoDataDomain", code: -1, userInfo: nil) } override func contents(forType typeName: String) throws -> Any { if let data = try? JSONEncoder().encode(self.note) { return data } //if we get here, there was some kind of problem throw NSError(domain: "NoDataDomain", code: -2, userInfo: nil) } } class Model { var documents = [NoteDocument]() var controller: UICollectionViewController? let noteFileExtension = ".note" var metadataQuery = NSMetadataQuery() init() { metadataQuery.notificationBatchingInterval = 1 metadataQuery.searchScopes = [NSMetadataQueryUbiquitousDataScope, NSMetadataQueryUbiquitousDocumentsScope] metadataQuery.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemFSNameKey, "*") metadataQuery.sortDescriptors = [NSSortDescriptor(key: NSMetadataItemFSNameKey, ascending: true)] metadataQuery.start() let notificationCenter = NotificationCenter.default notificationCenter.addObserver(self, selector: #selector(didFinish), name: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: nil) notificationCenter.addObserver(self, selector: #selector(didStart), name: NSNotification.Name.NSMetadataQueryDidStartGathering, object: nil) notificationCenter.addObserver(self, selector: #selector(didUpdate), name: NSNotification.Name.NSMetadataQueryDidUpdate, object: nil) } @objc func didStart(notification: NSNotification) { let query: NSMetadataQuery = notification.object as! NSMetadataQuery print("didStart found \(query.results.count) items") } @objc func didFinish(notification: NSNotification) { let query: NSMetadataQuery = notification.object as! NSMetadataQuery print("didFinish found \(query.results.count) items") } @objc func didUpdate(notification: NSNotification) { metadataQuery.disableUpdates() if let modifications = notification.userInfo { if let added = modifications[NSMetadataQueryUpdateAddedItemsKey] as? [NSMetadataItem] { for item in added { onAdd(item: item) } } } metadataQuery.enableUpdates() print("didUpdate") } func onAdd(item: NSMetadataItem) { if let url = item.value(forAttribute: NSMetadataItemURLKey) as? URL { if url.pathExtension == "note" { let document = NoteDocument(fileURL: url) document.open { success in if success { let note = document.note if !self.documents.contains(where: {$0.note.id == note.id}) { self.documents.append(document) if let controller = self.controller, let collectionView = controller.collectionView { DispatchQueue.main.async { collectionView.reloadData() //Update UI } } } } else { print("There was an error adding items") } } } } } }
1. 等待iCloud同步完成再打开文档
断网修改的内容联网后需要时间同步到本地,NSMetadataQuery触发更新时文件可能还未完成下载,此时调用open会报错。可以通过检查文件下载状态延迟打开:
func onAdd(item: NSMetadataItem) { guard let url = item.value(forAttribute: NSMetadataItemURLKey) as? URL, url.pathExtension == "note" else { return } // 检查文件是否已下载到本地 let isDownloaded = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String == NSMetadataUbiquitousItemDownloadingStatusCurrent if isDownloaded { openDocument(at: url) } else { // 订阅文件下载完成通知 let notificationCenter = NotificationCenter.default let observer = notificationCenter.addObserver(forName: .NSUbiquitousFileDownloaded, object: nil, queue: .main) { [weak self] notification in guard let downloadedURL = notification.userInfo?[NSUbiquitousFileURLKey] as? URL, downloadedURL == url else { return } notificationCenter.removeObserver(observer) self?.openDocument(at: url) } // 主动请求下载文件 NSFileManager.default.startDownloadingUbiquitousItem(at: url) { error in if let error = error { print("下载文件失败:\(error.localizedDescription)") notificationCenter.removeObserver(observer) } } } } private func openDocument(at url: URL) { let document = NoteDocument(fileURL: url) document.open { [weak self] success in guard let self = self else { return } if success { let note = document.note if !self.documents.contains(where: {$0.note.id == note.id}) { self.documents.append(document) if let controller = self.controller, let collectionView = controller.collectionView { DispatchQueue.main.async { collectionView.reloadData() } } } } else { print("打开文档失败") } } }
2. 优化NSMetadataQuery筛选条件
当前predicate匹配所有文件,会触发不必要的更新事件,建议只筛选目标扩展名文件:
metadataQuery.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemFSNameKey, "*\(noteFileExtension)")
3. 添加打开失败的重试逻辑
即使检查了下载状态,仍可能因网络波动打开失败,可添加重试机制:
private func openDocument(at url: URL, retryCount: Int = 3) { let document = NoteDocument(fileURL: url) document.open { [weak self] success in guard let self = self else { return } if success { let note = document.note if !self.documents.contains(where: {$0.note.id == note.id}) { self.documents.append(document) if let controller = self.controller, let collectionView = controller.collectionView { DispatchQueue.main.async { collectionView.reloadData() } } } } else if retryCount > 0 { // 延迟1秒重试 DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.openDocument(at: url, retryCount: retryCount - 1) } } else { print("多次尝试后打开文档仍失败") } } }
4. 监听iCloud账户状态变化
添加账户状态监听,避免在账户未正常连接时执行操作:
// 在Model的init方法中添加 NotificationCenter.default.addObserver(self, selector: #selector(handleiCloudAccountChange), name: .NSUbiquityIdentityDidChange, object: nil) @objc func handleiCloudAccountChange() { // 重置查询并刷新文档列表 metadataQuery.stop() metadataQuery.start() documents.removeAll() controller?.collectionView?.reloadData() }
内容的提问来源于stack exchange,提问作者FE_Tech

