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