Method for moving rows with NSFetchedResultsController and its delegate not working

1.6k Views Asked by At

I've been trying to follow this tutorial for rearranging rows in a table view while using NSFetchedResultsController and NSFetchedResultsController delegate, but my project is extremely buggy. What happens is that when I try rearranging a row and drop it, the rows are randomised (probably not randomised, but I can't see the pattern to find the problem). What's wrong with my implementation?

Bug

Table view data source:

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("RoutineCell", forIndexPath: indexPath) as! UITableViewCell
    var routine = fetchedResultsController.objectAtIndexPath(indexPath) as! Routine

    lastIndex++

    // Configure the cell...

    cell.textLabel?.text = routine.name

    return cell
}

// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}

// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        let managedContext = appDelegate.managedObjectContext!

        var routine = fetchedResultsController.objectAtIndexPath(indexPath) as! Routine

        // Delete the row from the data source
        managedContext.deleteObject(routine)
        managedContext.save(nil)
    } else if editingStyle == .Insert {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    }    
}

// Override to support conditional rearranging of the table view.
override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}

// Override to support rearranging the table view.
override func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {

    let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
    let managedContext = appDelegate.managedObjectContext!

    if var routines = fetchedResultsController.fetchedObjects {

        let routine = routines[sourceIndexPath.row] as! Routine

        println("Instance of routine: \(routines[sourceIndexPath.row].description) created")

        routines.removeAtIndex(sourceIndexPath.row)

        println("Fetched data is: \(routines.description)")

        println("Routine removed at \(routines[sourceIndexPath.row])")

        routines.insert(routine, atIndex: destinationIndexPath.row)

        println("Routine inserted at index \(destinationIndexPath.row)")

        var idx: Int = Int(routines.count)
        for routine in routines as! [Routine] {
            routine.index = idx--
        }
        managedContext.save(nil)
    }

    dispatch_async(dispatch_get_main_queue(), { () -> Void in
        tableView.reloadRowsAtIndexPaths(tableView.indexPathsForVisibleRows()!, withRowAnimation: UITableViewRowAnimation.Fade)
    })
}

NSFetchedResultsControllerDelegate methods:

func controllerWillChangeContent(controller: NSFetchedResultsController) {
    self.tableView.beginUpdates()
}

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
}

func controller(
    controller: NSFetchedResultsController,
    didChangeObject anObject: AnyObject,
    atIndexPath indexPath: NSIndexPath?,
    forChangeType type: NSFetchedResultsChangeType,
    newIndexPath: NSIndexPath?) {

        // implementation to follow...

        switch type {
        case .Insert:
            // Note that for Insert, we insert a row at the __newIndexPath__
            if let insertIndexPath = newIndexPath {
                self.tableView.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: .Fade)
            }
        case .Delete:
            // Note that for Delete, we delete the row at __indexPath__
            if let deleteIndexPath = indexPath {
                self.tableView.deleteRowsAtIndexPaths([deleteIndexPath], withRowAnimation: .Fade)
            }
        case .Update:
            // Note that for Update, we update the row at __indexPath__
            if let updateIndexPath = indexPath {
                let cell = self.tableView.cellForRowAtIndexPath(updateIndexPath)
                let routine = self.fetchedResultsController.objectAtIndexPath(updateIndexPath) as? Routine

                cell?.textLabel?.text = routine?.name
            }
        case .Move:
            // Note that for Move, we delete the row at __indexPath__
            if let deleteIndexPath = indexPath {
                self.tableView.deleteRowsAtIndexPaths([deleteIndexPath], withRowAnimation: .Fade)
            }

            // Note that for Move, we insert a row at the __newIndexPath__
            if let insertIndexPath = newIndexPath {
                self.tableView.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: .Fade)
            }
        }
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    self.tableView.endUpdates()
}

I did the same moves as in the gif above:

Console output Console output 2

1

There are 1 best solutions below

2
On BEST ANSWER

Your routine updating the data model is a bit difficult to read. You are assigning the highest index to the record on top of the list counting down to 1 and presumably reverse sorting... OK, let's assume that is working. *)

Perhaps you should simply disable the delegate methods when you process user generated changes of the data? Bracket your operations including the save with

self.fetchedResultsController.delegate = nil
// ...
self.fetchedResultsController.delegate = self

*) I think that if you were initially counting up and then reorder counting down with the fetched results controller counting up you would get the kind of errors illustrated in your image.