Swift Core Data统计布尔属性实体数及UITableView固定分区咨询
问题解答
1. 新建第二个fetch请求的影响
正常业务场景下不会引发严重的性能或逻辑问题,但有两个细节需要注意:
- 性能层面:如果单场考试关联的Topic数量在千级以内,就算发第二个带谓词的fetch请求,只要为
topicIsCompleted属性、Exam和Topic的关联外键建立了索引,查询耗时基本感知不到。但要注意没必要每次刷新UI都发起新的fetch,避免无意义的磁盘IO。 - 逻辑层面:单独的fetch请求不会自动和
NSFetchedResultsController(以下简称FRC)的数据变化同步,如果统计逻辑没有和FRC的内容更新回调绑定,很容易出现统计数字和列表显示不一致的问题。只要把统计触发逻辑放在FRC的controllerDidChangeContent(_:)回调中统一执行,就能避免逻辑不同步。
2. 固定双分区、允许空分区的实现可行性
完全可以实现,这是解决分区识别问题性价比极高的方案,核心实现逻辑如下:
- 不要直接把FRC返回的
sections数组作为表视图的唯一分区数据源,固定约定分区0为「待完成」、分区1为「已完成」,两个分区的标题固定写死。 - 表视图数据源方法
numberOfSections(in:)固定返回2,不要根据FRC的分区数量动态返回。 - 在
tableView(_:numberOfRowsInSection:)方法中,根据传入的section匹配要查询的完成状态,从FRC的sections中查找对应状态的分组,找到就返回该分组的行数,找不到直接返回0即可。 - 必须拦截FRC的分区变更回调:FRC默认会在某一状态的Topic数量从1降到0、或者从0升到1时,触发分区删除/插入的回调,这时候如果直接把这类事件透传给表视图,会触发数据源不一致崩溃。你只需要在
controller(_:didChangeSection:atIndex:forChangeType:)方法中直接跳过所有分区级别的变更处理,仅响应行级别的增、删、改、移动事件即可。
核心实现代码参考:
func numberOfSections(in tableView: UITableView) -> Int { return 2 } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return section == 0 ? "待完成" : "已完成" } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let isCompletedSection = (section == 1) guard let sections = fetchedResultsController.sections else { return 0 } let targetSection = sections.first { guard let firstTopic = $0.objects?.first as? Topic else { return false } return firstTopic.topicIsCompleted == isCompletedSection } return targetSection?.numberOfObjects ?? 0 } // 拦截FRC的分区变更,不做任何处理 func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { return }
3. 更优替代方案
按实现成本和性能排序,有两个比新建fetch请求更好的方案:
- 方案一(最轻量,优先推荐):直接在内存中遍历关联Topic统计
你之前用valueForKey获取布尔属性统计失效,是因为Core Data托管对象的布尔属性会被包装为NSNumber类型,直接做指针相等判断会出错,只要取boolValue后再判断即可。这个方案完全不需要额外发起磁盘查询,零额外IO开销:
只要把这段统计逻辑放在FRC的guard let topicSet = currentExam.topics as? Set<Topic> else { // 无数据时计数清零 completedCount = 0 pendingCount = 0 return } completedCount = topicSet.filter(\.topicIsCompleted).count pendingCount = topicSet.count - completedCountcontrollerDidChangeContent(_:)回调中执行,统计结果会和列表实时保持一致。 - 方案二(维护成本最低):和固定双分区方案结合,直接取分区行数做统计
实现完第二点提到的固定双分区逻辑后,你根本不需要额外写遍历或者fetch的统计逻辑,直接调用表视图的数据源方法,取两个分区的行数就是待完成、已完成的Topic数量,连单独的统计代码都不需要维护,是长期迭代最稳定的方案。
如果后续单场考试关联的Topic量级达到万级以上,可以改用带countResultType的fetch请求做计数,这类请求不会把完整托管对象加载到内存,只会返回统计数值,性能比普通fetch高很多,但普通业务场景基本用不到。
内容的提问来源于stack exchange,提问作者Lukenn77
相关产品推荐
相关产品推荐

