使用NSFetchedResultsController的countResultType监听变更崩溃求助
尝试用NSFetchedResultsController配合countResultType类型的NSFetchRequest获取Core Data中的对象数量,初始加载数据时能正常返回计数,但数据变更触发代理方法时应用崩溃,报错信息如下:
Thread 1: "Result type of fetched object is not an NSManagedObjectIDResultType, NSDictionaryResultType or NSManagedObjectResultType."
用户提供的代码片段:
查询请求代码
var fetchRequest: NSFetchRequest<NSFetchRequestResult> { let fetchRequest: NSFetchRequest<NSFetchRequestResult> fetchRequest = MessageEntity.fetchRequest() fetchRequest.resultType = NSFetchRequestResultType.countResultType let senderPredicate = NSPredicate( format: "senderUserId = %@", contactId ) let recipientPredicate = NSPredicate( format: "recipientUserId = %@", contactId ) fetchRequest.predicate = NSCompoundPredicate( orPredicateWithSubpredicates: [ senderPredicate, recipientPredicate ] ) let sortDescriptor = NSSortDescriptor(key: "date", ascending: false) fetchRequest.sortDescriptors = [sortDescriptor] return fetchRequest }
查询结果控制器代码
lazy var fetchResultsController: NSFetchedResultsController<NSFetchRequestResult> = { fetchRequest.resultType = NSFetchRequestResultType.countResultType let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) fetchedResultsController.delegate = self return fetchedResultsController }()
初始加载与变更监听代码
func loadMessagesFromCoreData() { print("loadMessagesFromCoreData: Thread is \(Thread.current) main? \(Thread.isMainThread)") Task { [weak self] in do { try self?.fetchResultsController.performFetch() guard let entities = self?.fetchResultsController.fetchedObjects else { throw AppAlert.error(error: "unable to load messages from core data") } print("receiving local messages on load \(String(describing: entities.last))") } catch { print("Error loading messages from core data:", error.localizedDescription) } } } func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { guard let entities = controller.fetchedObjects else { return } print("receiving local messages on change \(String(describing: entities.last))") }
崩溃原因
NSFetchedResultsController(简称FRC)的设计初衷是管理实体对象、实体ID或字典格式的查询结果,并不支持countResultType这种仅返回单一数值的结果类型。虽然初始调用performFetch()时能拿到计数,但当数据发生变更时,FRC内部会自动更新结果集,此时会校验结果类型,发现不符合支持的类型范围,直接抛出错误导致崩溃。
解决方案
方案1:不使用FRC,直接监听上下文变更计数
如果只需要获取数量,不需要管理实体列表,可以直接监听NSManagedObjectContext的对象变更通知,每次收到通知后重新执行计数查询:
// 在初始化方法(如init、viewDidLoad)中添加监听 override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver( self, selector: #selector(updateMessageCount), name: NSManagedObjectContext.objectsDidChangeNotification, object: context ) } // 执行计数查询的方法 @objc private func updateMessageCount() { do { // 构造轻量计数请求,移除排序以优化性能 let countRequest = MessageEntity.fetchRequest() countRequest.predicate = fetchRequest.predicate let count = try context.count(for: countRequest) print("当前消息数量:\(count)") } catch { print("计数查询失败:\(error.localizedDescription)") } } // 在对象销毁时移除监听 deinit { NotificationCenter.default.removeObserver(self) }
这种方案更轻量,不需要维护FRC,适合只需要计数的场景。
方案2:用FRC管理实体,通过列表count获取数量
如果希望保留FRC的变更监听能力(比如需要感知具体的增删改操作),可以修改请求的结果类型为默认的NSManagedObjectResultType,通过fetchedObjects?.count得到数量:
- 修改查询请求:
var fetchRequest: NSFetchRequest<MessageEntity> { let fetchRequest = MessageEntity.fetchRequest() // 移除countResultType设置,使用默认的NSManagedObjectResultType let senderPredicate = NSPredicate( format: "senderUserId = %@", contactId ) let recipientPredicate = NSPredicate( format: "recipientUserId = %@", contactId ) fetchRequest.predicate = NSCompoundPredicate( orPredicateWithSubpredicates: [ senderPredicate, recipientPredicate ] ) let sortDescriptor = NSSortDescriptor(key: "date", ascending: false) fetchRequest.sortDescriptors = [sortDescriptor] return fetchRequest }
- 修改FRC定义:
lazy var fetchResultsController: NSFetchedResultsController<MessageEntity> = { let fetchedResultsController = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil ) fetchedResultsController.delegate = self return fetchedResultsController }()
- 修改初始加载与代理方法:
func loadMessagesFromCoreData() { print("loadMessagesFromCoreData: Thread is \(Thread.current) main? \(Thread.isMainThread)") Task { [weak self] in do { try self?.fetchResultsController.performFetch() let count = self?.fetchResultsController.fetchedObjects?.count ?? 0 print("初始加载消息数量:\(count)") } catch { print("Error loading messages from core data:", error.localizedDescription) } } } func controllerDidChangeContent(_ controller: NSFetchedResultsController<MessageEntity>) { let count = controller.fetchedObjects?.count ?? 0 print("数据变更后消息数量:\(count)") }
这种方案可以利用FRC的代理方法精准感知数据变化,同时通过实体列表的count属性得到数量,完全避免了原有的崩溃问题。
内容的提问来源于stack exchange,提问作者abed

