You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 08:13:11