1

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.

enter image description here

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:

enter image description here

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()
    }
}
austintt
  • 485
  • 1
  • 4
  • 13
  • 1
    I would drop `NSFetchedResultsController` and update the datasource *manually* after receiving the `NSManagedObjectContextDidSave` notification. The diffable data source does the rest (Including the animations). `NSFetchedResultsController` is only useful if the data source is linear or the controller manages the sections, too. This avoids also the misused `saveContext` in `didSelect` – vadian Apr 25 '20 at 18:52
  • @vadian that did the trick! Thank you! Do you want to post that as an answer so I can accept it? – austintt Apr 26 '20 at 22:16
  • what will happen if you have 1000's of rows, won't it be an overhead for updating datasource everytime. – Ankit Thakur Jul 05 '20 at 15:32

0 Answers0