使用 NSFetchedResultsController 如何以编程方式设置 5 个部分以显示以及过滤它们以获取适当的行

Posted

技术标签:

【中文标题】使用 NSFetchedResultsController 如何以编程方式设置 5 个部分以显示以及过滤它们以获取适当的行【英文标题】:Using NSFetchedResultsController how can I programmatically set 5 sections to show up as well as filter them to get the appropriate rows 【发布时间】:2016-05-29 20:51:55 【问题描述】:

在这里重新迭代是我的困境,我正在重构我的代码以使用 NSFetchedResultsController,我希望对一组 5 个部分进行硬编码,无论它们是否有任何行或不是。我在让它们出现方面取得了一定的成功,但不幸的是,我无法让适当的行出现在正确的部分下。 (注意:我正在使用带有按钮的自定义标题,当用户想要将单元格添加到任何特定部分时,该按钮会添加行。) coreData 中的模型关系是 Dog 模型可以有许多 Task,但 Task 只能有一只狗。不幸的是,我似乎正在为我创建的每一个独特的“狗”获取所有任务,因此它们都有相同的信息。

这是我的任务模型和我用来在 NSFetchedResultsController 中创建 5 个部分的枚举。

import Foundation
import CoreData

enum Type: String 
    case Meals = "Meals"
    case Exercise = "Exercise"
    case Health = "Health"
    case Training = "Training"
    case Misc = "Misc"



class Task: NSManagedObject 
    static let kClassName = "Task"

    convenience init?(title: String, type: Type, isComplete: Bool, context: NSManagedObjectContext = Stack.sharedStack.managedObjectContext) 
        guard let entity = NSEntityDescription.entityForName(Task.kClassName, inManagedObjectContext: context) else  return nil 

        self.init(entity: entity, insertIntoManagedObjectContext: context)
        self.title = title
        self.isChecked = isComplete
        self.type = type.rawValue
    

这是我的 FetchedResultsController,我将枚举类型作为我的 sectionNameKeyPath 传递。

class TaskController 
    static let sharedController = TaskController()
    private let kTask = "Task"
    var fetchedResultsController: NSFetchedResultsController

    var dog: Dog?

    init() 
        let request = NSFetchRequest(entityName: kTask)
        let sortDescriptor1 = NSSortDescriptor(key: "type", ascending: true)
        request.sortDescriptors = [sortDescriptor1]

        fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: Stack.sharedStack.managedObjectContext, sectionNameKeyPath: String(Type), cacheName: nil)
        _ = try? fetchedResultsController.performFetch()
    

在 numberOfRowsInSection 中,我试图将sections.count 匹配为等于函数属性部分的值,因为我假设 fetchedResultsController 部分在首先创建行之前实际上并不存在?老实说,我在这一点上迷路了,因为我不确定如何获取正确的行以匹配适当的部分。在我决定重构代码并将其更新为 NSFetchedResultsController 方式之前,注释代码是我最初检索正确部分的正确行的方式。

extension DogDetailViewController: UITableViewDataSource 
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
        guard let sections = TaskController.sharedController.fetchedResultsController.sections else  return 0 

        if sections.count > 0 && section < sections.count 
            return sections[section].numberOfObjects
        
        return 0

            //            

            //        if let dog = self.dog 
            //            switch section 
            //            case 0:
            //                return dog.tasks.filter($0.type == String(Type.Meals)).count
            //            case 1:
            //                return dog.tasks.filter($0.type == String(Type.Exercise)).count
            //            case 2:
            //                return dog.tasks.filter($0.type == String(Type.Health)).count
            //            case 3:
            //                return dog.tasks.filter($0.type == String(Type.Training)).count
            //            case 4:
            //                return dog.tasks.filter($0.type == String(Type.Misc)).count
            //            default:
            //                return 0
            //            
            //         else 
            //            return 0
            //        
        

感谢任何帮助,谢谢!

【问题讨论】:

【参考方案1】:

如果您希望所有 5 个部分都显示出来,无论给定的狗存在多少该类型的任务,您都可以考虑对部分的数量及其顺序进行硬编码,并使用 NSFetchedResultsController(因此不同NSFetchRequest) 每个部分。

您现有的提取请求不会将您的请求限制为特定的狗(您可以通过创建具有适当约束的NSPredicate 并将其设置在您的提取请求中来做到这一点)。这可能很重要,因此您不要将每个Task 都拉入内存然后执行过滤器。这将要求您的 Task 实体与数据模型中的 Dog 实体有关系,根据您发布的代码,这并不明显。我会假设你有这种关系。您也不能像现在这样为您的TaskController(下面的TaskDataSource)使用单例模型,因为每次更改您想要制作的Dog 时都必须创建一个新模型请求。

还请注意,我只是将TaskController 变成了TaskDataSource,并直接采用了UICollectionViewDataSource 协议。这不是必需的,但如果您的应用可能希望在多个屏幕上显示此内容,则它可能更易于重复使用。

class TaskDataSource: NSObject, UICollectionViewDataSource 
    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int 
        return TaskDataSource.Sections.count
    

    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
        let count: Int
        switch TaskDataSource.Sections[section] 
        case .Meals:
            count = mealsResultsController?.sections?.first?.numberOfObjects ?? 0
        case .Exercise:
            count = exerciseResultsController?.sections?.first?.numberOfObjects ?? 0
        case .Health:
            count = healthResultsController?.sections?.first?.numberOfObjects ?? 0
        case .Training:
            count = trainingResultsController?.sections?.first?.numberOfObjects ?? 0
        case .Misc:
            count = miscResultsController?.sections?.first?.numberOfObjects ?? 0
        

        return count
    

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell 
        let task: Task
        // Each fetched results controller has a singel section, so we have to make an appropriate index path
        let adjustedIndexPath = NSIndexPath(forItem: indexPath.item, inSection: 0)
        switch TaskDataSource.Sections[indexPath.section] 
        case .Meals:
            task = mealsResultsController?.objectAtIndexPath(adjustedIndexPath) as! Task
        case .Exercise:
            task = exerciseResultsController?.objectAtIndexPath(adjustedIndexPath) as! Task
        case .Health:
            task = healthResultsController?.objectAtIndexPath(adjustedIndexPath) as! Task
        case .Training:
            task = trainingResultsController?.objectAtIndexPath(adjustedIndexPath) as! Task
        case .Misc:
            task = miscResultsController?.objectAtIndexPath(adjustedIndexPath) as! Task
        

        // This part will vary, depending on your cell / storyboard, but this is the idea.  Note we don't use the adjusted index path here
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("TaskCell", forIndexPath: indexPath) as! TaskCell
        cell.titleLabel.text = task.title

        return cell
    

    init(dog: Dog) 
        // Create a sort descriptor to sort by whatever you like, I assume you'd want things sorted by title
        let sortDescriptor = NSSortDescriptor(key: "title", ascending: true)

        // A closure to create an NSFetchedResultsController, this avoids copy/pasting
        let createFetchRequestForType =  (type: Type) -> NSFetchedResultsController? in
            let fetchRequest = NSFetchRequest(entityName: Task.kClassName)
            // Note, you'll want to create a multi-key index on the Task entity to make sure this is reasonably fast
            fetchRequest.predicate = NSPredicate(format: "dog == %@ && type == %@", dog, type.rawValue)
            fetchRequest.sortDescriptors = [sortDescriptor]

            let context = Stack.sharedStack.managedObjectContext
            let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
            do 
                try fetchedResultsController.performFetch()
            
            catch 
                return nil
            

            return fetchedResultsController
        

        mealsResultsController = createFetchRequestForType(.Meals)
        exerciseResultsController = createFetchRequestForType(.Exercise)
        healthResultsController = createFetchRequestForType(.Health)
        trainingResultsController = createFetchRequestForType(.Training)
        miscResultsController = createFetchRequestForType(.Misc)
    

    static let Sections: Array<Type> = [.Meals, .Exercise, .Health, .Training, .Misc]

    var mealsResultsController: NSFetchedResultsController?
    var exerciseResultsController: NSFetchedResultsController?
    var healthResultsController: NSFetchedResultsController?
    var trainingResultsController: NSFetchedResultsController?
    var miscResultsController: NSFetchedResultsController?

【讨论】:

谢谢@Charles A.,今晚晚些时候再试一试。有趣的是,您为 Sections 数组中的每个项目分配了自己的 NSFetchedResultsController 变量分配。我只是假设我只需要一个主要的 NSFetchedResultsController 来过滤我想要的信息? 如果您想在特定类型的任务不存在时完全省略该部分,我会使用一个NSFetchedResultsController,在NSPredicate 中省略对type 的约束,而是拆分使用sectionNameKeyPath 将其分成几个部分。不过,鉴于您总是想要这些部分,我觉得这是一种更简单的方法。理想情况下,您希望在合理的情况下使用谓词在数据库引擎中进行过滤,而不是在内存中。

以上是关于使用 NSFetchedResultsController 如何以编程方式设置 5 个部分以显示以及过滤它们以获取适当的行的主要内容,如果未能解决你的问题,请参考以下文章

核心数据保存竞争条件错误

在 Swift 3 中难以配置 NSFetchedResultsController

为啥 beginUpdates/endUpdates 会重置表视图位置以及如何阻止它这样做?

测试使用

第一篇 用于测试使用

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?