I've had trouble getting my Core Data objects with one-to-many relationships to correctly work with DiffableDataSource and could use some pointers.
I have a table view that displays Task
objects retrieved from Core Data. I'm using a combination of DiffableDataSource
and NSFetchedResultsController
. The Task object is modeled such that it has a one-to-many "subtasks" relationship to more Task objects. The table view should display all parent level tasks. When a parent task is tapped, it's subtasks should be inserted or removed from the tableview below its position.
I've gone through several different solutions in my didChangeContentWith
method below, but haven't reached the results I want. Currently, my tableview appears to be all scrambled; selecting one row inserts duplicate or incorrect tasks.
At first I thought my cells weren't properly prepared for reuse, but that does not seem to be the case. I've also tried only fetching parent Tasks and using their references to subtasks for insertion, adding all tasks and deleting subtasks when not expanded, and a couple other methods.
When I take a look at the initial snapshot and final diff, in my didChangeContentWith
method, the initial snapshot contains all Tasks (and subtasks), the diff contains just the desired Tasks. I'm not sure what is happening in between my diff and its application to the table view. I have the feeling that this has to do with a Task's reference to more Tasks not playing nice with Diffable, but I'm probably just crazy.
The example data and logic is as follows:
{
title: "Test"
subtasks: [
title: "1",
title: "2",
title: "3"
],
title: "Foo"
subtasks: [
title: "bod",
title: "bat",
title: "bar"
],
title: "Bloop"
subtasks: [
title: "Blah1",
title: "Blah2",
title: "Blah3"
]
}
Task Model:
Instantiation of the NSFetchedResultsController
:
lazy var fetchedResultsController: NSFetchedResultsController<Task> = {
let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
let nameSort = NSSortDescriptor(key: #keyPath(Task.title), ascending: true)
let dueDate = NSSortDescriptor(key: #keyPath(Task.dueDate), ascending: false)
fetchRequest.sortDescriptors = [dueDate, nameSort]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.managedContext, sectionNameKeyPath: nil, cacheName: "taskMinder")
fetchedResultsController.delegate = self
return fetchedResultsController
}()
Configuration of the dataSource:
func configure(cell: UITableViewCell, for indexPath: IndexPath) {
guard let cell = cell as? TaskTableViewCell else { return }
let task = fetchedResultsController.object(at: indexPath)
cell.set(task: task)
}
func configureDataSource() -> UITableViewDiffableDataSource<String, Task> {
return UITableViewDiffableDataSource(tableView: tableView) { [unowned self] (tableView, indexPath, task) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: TaskTableViewCell.reuseID, for: indexPath)
self.configure(cell: cell, for: indexPath)
return cell
}
}
And finally, I have the following delegate methods:
extension TasksViewController: NSFetchedResultsControllerDelegate {
func controller( _ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
// Create diff and go through sections
var diff = NSDiffableDataSourceSnapshot<String, Task>()
snapshot.sectionIdentifiers.compactMap { $0 as? String }.forEach { section in
diff.appendSections([section])
let tasks = snapshot.itemIdentifiersInSection(withIdentifier: section).compactMap { $0 as?
NSManagedObjectID }.compactMap {
controller.managedObjectContext.object(with: $0) as? Task
}
tasks.forEach { task in
if let parent = task.parentTask, parent.isExpanded {
diff.insertItems([task], afterItem: parent)
} else if task.parentTask == nil {
diff.appendItems([task], toSection: section)
}
}
}
// Apply the diff
dataSource?.apply(diff)
}
}
extension TasksViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let task = dataSource?.itemIdentifier(for: indexPath) else { return }
tableView.deselectRow(at: indexPath, animated: true)
// Hide/show subtasks
if task.subtasks != nil {
task.isExpanded.toggle()
if let cell = tableView.cellForRow(at: indexPath) as? TaskTableViewCell {
UIView.animate(withDuration: 0.2) {
cell.isExpanded = task.isExpanded
}
}
} else {
// toggle completion status
}
self.coreDataStack.saveContext()
}
}