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

Core Data保存后NSFetchedResultsController仍用临时ID致崩溃问题

问题分析

崩溃的核心原因是:新创建的Asset对象在Core Data上下文保存前使用的是临时NSManagedObjectID,调用context.save()后,Core Data会将临时ID替换为持久化的永久ID,但你手动构建的快照里存储的仍然是保存前的临时ID。当用户点击单元格时,用这个临时ID查找对象,此时临时ID对应的对象已被替换为永久ID的对象,自然找不到,抛出133000错误。

解决方案

方案1:直接使用FRC提供的快照(推荐)

FetchedResultsController在didChangeContentWith回调里提供的NSDiffableDataSourceSnapshotReference已经处理过永久ID,无需手动构建快照,直接转换后应用即可:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
    guard let newSnapshot = snapshot as? NSDiffableDataSourceSnapshot<String, NSManagedObjectID> else {
        print("快照类型不匹配")
        return
    }
    dataSource.apply(newSnapshot, animatingDifferences: true)
}

方案2:手动构建快照时确保获取永久ID

如果必须手动构建快照,需确保上下文保存完成后,从FRC结果中获取Asset的永久ID,同时修正FRC配置中的关系预取错误:

修正FRC获取函数

func performCoreDataFetch(){
    guard let context = self.context else { return }
    let fetchRequest = Group.fetchRequest()
    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)]
    // 修正预取关系名,对应Group里的一对多关系`assets`
    fetchRequest.relationshipKeyPathsForPrefetching = ["assets"]
    fetchRequest.predicate = NSPredicate(format: "ANY assets != nil")

    fetchResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
                                                        managedObjectContext: context,
                                                        sectionNameKeyPath: "name",
                                                        cacheName: nil)
    fetchResultsController.delegate = self
    do {
        try fetchResultsController.performFetch()
    } catch {
        print("Fetch失败:\(error)")
    }
}

修正快照构建逻辑

确保构建快照时,Asset的ID已为永久ID(上下文保存后FRC会自动更新结果):

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
    var newSnapshot = NSDiffableDataSourceSnapshot<String, NSManagedObjectID>()
   
    guard let sections = controller.sections else { return }
    for sectionInfo in sections {
        guard let group = sectionInfo.objects?.first as? Group, let groupName = group.name else { continue }
        
        newSnapshot.appendSections([groupName])
        
        // 从Group中获取已保存的Asset,过滤临时ID
        let permanentIDs = group.assets?.compactMap { asset in
            let id = asset.objectID
            return id.isTemporaryID ? nil : id
        } ?? []
        newSnapshot.appendItems(permanentIDs, toSection: groupName)
    }
    
    dataSource.apply(newSnapshot, animatingDifferences: true)
}

额外优化点

  1. Cell注册的错误处理:避免强制try,增加容错逻辑:
let assetCell = UICollectionView.CellRegistration<CustomAssetCell, NSManagedObjectID> { (cell, indexPath, objectID) in            
    guard let context = self.context else { return }
    do {
        if let asset = try context.existingObject(with: objectID) as? Asset {
            cell.imageView.image = asset.image 
        }
    } catch {
        print("加载Cell数据失败:\(error)")
        cell.imageView.image = UIImage(systemName: "photo") // 设置默认占位图
    }
}
  1. 点击事件的安全校验:提前过滤临时ID,避免无效查找:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    guard let objectID = self.dataSource.itemIdentifier(for: indexPath) else {
        collectionView.deselectItem(at: indexPath, animated: true)
        return
    }
    
    // 跳过临时ID,避免无效查找
    guard !objectID.isTemporaryID else {
        print("无效的临时ObjectID")
        collectionView.deselectItem(at: indexPath, animated: true)
        return
    }
    
    guard let context = self.context else {
        collectionView.deselectItem(at: indexPath, animated: true)
        return
    }
    
    do {
        guard let asset = try context.existingObject(with: objectID) as? Asset else { 
            collectionView.deselectItem(at: indexPath, animated: true)
            return 
        }                
        
        // 跳转详情页逻辑
        if let assetDetail = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: MasterListViewController.assetDetailIdentifier) as? DetailViewController {
            assetDetail.context = self.context
            assetDetail.asset = asset
            let navController = UINavigationController(rootViewController: assetDetail)
            present(navController, animated: true)
        }
    } catch {
        print("获取对象失败:\(error)")
        collectionView.deselectItem(at: indexPath, animated: true)
    }
}

内容的提问来源于stack exchange,提问作者robinyapockets

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 13:52:04