Search code examples
swiftcore-datafilternsfetchedresultscontrollernsfetchrequest

Using NSFetchedResultsController how can I programmatically set 5 sections to show up as well as filter them to get the appropriate rows


To re-iterate here is my dilemma, I'm refactoring my code to use NSFetchedResultsController, I wish to hard code a set of 5 sections which I want to show up at all times regardless if there are any rows in them or not. I am somewhat successful in getting them to show up but unfortunately I cannot get the appropriate rows to show up under the correct sections. (Note: I'm using a custom header with a button that adds row's whenever the user want to add a cell to any specific section.) The model relationship in coreData is that a Dog model can have many Task's but a Task can only have one Dog. Unfortunately I seem to be fetching all the tasks for every unique "Dog" that I create so they all have the same information.

Here is my Task model and the enum I'm using to create the 5 sections in the NSFetchedResultsController.

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
    }
}

Here's my FetchedResultsController where I'm passing the Enum Type as my 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()
    }
}

In the numberOfRowsInSection I'm trying to match the sections.count to be equal to that of the function property section as I'm assuming the fetchedResultsController section does not actually exist until a row is created first? To be honest I'm lost at this point, as I'm not sure how to fetch the correct rows to match the appropriate sections. The commented code was how I was initially retrieving the correct rows for the correct sections before I decided to refactor my code and updated it to the NSFetchedResultsController way.

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
            //        }
        }

Any help is appreciated, Thanks!


Solution

  • If you want all 5 sections to show up, regardless of how many tasks of that type exist for a given dog, you might consider hard coding the number of sections and their order and having an NSFetchedResultsController (and thus a different NSFetchRequest) for each section.

    Your existing fetch request is not constraining your request to a specific dog (you would do this by creating an NSPredicate with the appropriate constraint and setting it on your fetch request). This is probably important to do, so you don't pull every Task into memory and then perform the filter. This would require that your Task entity has a relationship to the Dog entity in your data model, which isn't obvious based on the code you've posted. I will assume that you have this relationship. You also can't use a singleton model for your TaskController (TaskDataSource below) in the way you are now, as you'll have to make a new one every time you change the Dog you want to make a request for.

    Note also that I just went ahead and made TaskController into TaskDataSource and adopted the UICollectionViewDataSource protocol directly. This isn't necessary, but makes this potentially more re-usable if your app might want this content on more than one screen.

    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?
    }