简体   繁体   中英

Reorder TableView Cells With NSFetchedResultsController Core Data - Swift 3

I am using an NSFetchedResultsController. I can't find any straightforward tutorials that walk through it for Swift 3 .

So here is what I have done so far. I have successfully populated my table using an NSFetchedResultsController that fetches the inserted data from core data. I created an attribute in my core data model called, orderPosition that is declared as Int32 . I haven't done anything with this attribute in regards to adding and saving data to core data upon insert.

In my fetch func where I initialize the NSFetchedResultsController , I have modified my sort descriptor to include the following code:

let sortDescriptor = NSSortDescriptor(key: "orderPosition", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]

Then I implemented the tableView func:

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {

    attemptFetch()
    var objects = self.controller.fetchedObjects! 
    self.controller.delegate = nil

    let object = objects[sourceIndexPath.row]
    objects.remove(at: sourceIndexPath.row)
    objects.insert(object, at: destinationIndexPath.row)

    var i = 0
    for object in objects {
        object.setValue(i += 1, forKey: "orderPosition")
    }

    appdel.saveContext()
    self.controller.delegate = self

}

In my implementation of the didChange anObject func, I included the switch-case for .insert , .delete , .update , and .move . My .move case looks like the following:

case.move:
        if let indexPath = indexPath {
            tableView.deleteRows(at: [indexPath], with: .fade)
        }
        if let indexPath = newIndexPath {
            tableView.insertRows(at: [indexPath], with: .fade)
        }
        break

Please help me figure this out. I have been spending weeks trying to understand this. I have gone through many if not all stack overflow questions on this. I implemented some of the loop ideas from one because I had a similar thought process to approach this but it didn't work. There are no Swift 3 core data tutorials/videos that I have come across that will really help me solve and understand this well. I don't want to give up on this.

Swift deviates from C in that the =, += and other assignment-like operations returns Void . So your line object.setValue(i += 1, forKey: "orderPosition") is setting i to be one more but is always setting the position to be 0. Try instead:

var i = 0
for object in objects {
    i += 1
    object.setValue(i, forKey: "orderPosition")
}

or if you want to simplify your code a bit you can do

for (i, object) in objects.enumerated() {
    object.setValue(i, forKey: "orderPosition")
}

Moving around:

    var objects = frc.fetchedObjects!

    // Disable fetchedresultscontroller updates.

    let obj objects.remove(at: indexPath.item)
    objects.insert(obj, at: newIndexPath)

    for (i, obj) in objects.enumerated() {
        obj.orderIndex = Int32(i)
    }

    // Enable fetchedresultscontroller updates.

    context.saveContext()

TLDR: Disable updates from the fetchedresultscontroller cause else things crash on you, or use a workaround which does not crash on you.

My full tale on why to lock.

The disable stuff is there cause the tableview moved the cell already. If you then set the orderIndex, it will try to move things again which might crash on you as it did on me.

My workaround to this is to not have a .move case

    case .update, .move:
        print("update")
        self.reloadRows(at: [indexPath!], with: .automatic)

which I do not need anyway cause I move only with the tableview for now. If you want to have the fetchedresultscontroller move stuff too you have to not have this case .update, .move but handle the .move yourself like

    case .move:
        print("move")
        self.moveRow(at: indexPath!, to: newIndexPath!)

but don't forget to implement the locking or my alternative below then.

Another workaround to not crash on me on endUpdates was

    if let toIndexPath = newIndexPath {
        if type == .update && toIndexPath != indexPath!{
            print("it should be a move.")
            print("move")
            self.moveRow(at: indexPath!, to: newIndexPath!)
            return
        }
    }

which detects updates for which newIndexPath != indexPath(the old index path), essentially it detects moves which are not declared as moves and fixed the problem for me too, but it was just too weird for me.

The following is the fetchedresultscontrollers' answer to my orderIndex changes, which should explain why you, incase you want to have the fetchedresultscontroller move stuff to better implement my "move disguised as update" detection or disable updates to table during the updating of the objects, which is fine cause the table reordered cells itself already by your move.

    from: Optional([0, 2])
    to: Optional([0, 1])
    update
    from: Optional([0, 1])
    to: Optional([0, 0])
    update
    from: Optional([0, 0])
    to: Optional([0, 2])
    move
    ... Assertion failure in ...
    attempt to perform an insert and a move to the same index path (<NSIndexPath: 0xc000000000400016> {length = 2, path = 0 - 2})
    (null)

edit:

I think the "move disguised as update" detection does not produce crap cause it simply reloads all the cells. Cause then it moved the tableviewcell 1 time by the tableview itself + 3 times by the detection(useless).

Why is fetchedresultscontroller using this call-pattern? It probably frantically tries to get the table in order as if the updates came from outside and the table needs updating. But it does not know that the table cells got put in the right order already by the tableview, so it crashes hard on super.endUpdates() for some reason.

SOO: probably most simple solution is to disable updates to the tableview. How? No idea right now, I'm going to bed.

Just implemented similar functionality in my project.Turned out to be easier than it seemed.

Here is an update for your moveRowAt method

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {

    // NOT NEEDED, SINCE I ASSUME THAT YOU ALREADY FETCHED ON INITIAL LOAD
    // attemptFetch()

    var objects = self.controller.fetchedObjects! 

    // NOT NEEDED
    //self.controller.delegate = nil

    let object = objects[sourceIndexPath.row]
    objects.remove(at: sourceIndexPath.row)
    objects.insert(object, at: destinationIndexPath.row)

    // REWRITEN BELOW
    //var i = 0
    //for object in objects {
    //    object.setValue(i += 1, forKey: "orderPosition")
    //}

    for (index, item) in items.enumerated() {
        item.orderPosition = index
    }

    appdel.saveContext()
    //self.controller.delegate = self

}

In didChange technically you don't need any code. The table is updated in place, when you move the item in it. And your data in coreData is updated when you save the context. So next time the data is fetched, based on the sortDescriptor you have, it will come in sorted by orderPosition.

Worked for me. Let me know, if you have any questions.

Assuming core data & fetchedResultsController: regarding moveRowAt, the from and to index path rows are within the section. If you are moving between the sections you need to work out the actual row to move in your array. ie: if you are moving the second row in the third section to the second row in the first section you actually need to know how many rows there are in all the sections to remove the correct row and insert at the correct place.

I use the following method to get the correct rows:-

    // Do not trigger delegate methods when changes are made to core data by the user
        fetchedResultsController.delegate = nil
        var fromIndex = fromIndexPath.row
        var toIndex = toIndexPath.row
        //      print ("from row ",fromIndexPath.row)
//work out correct row to remove at based on it's current row and section
        for sectionIndex in 0..<fromIndexPath.section
        {
            fromIndex += fetchedResultsController.sections![sectionIndex].numberOfObjects
        }
            //print ("fromIndex ",fromIndex)
// remove the row at it's old position
       toDoData.remove(at: fromIndex)
//work out the correct row to insert at based on which section it's going to & row in that section
        for sectionIndex in 0..<toIndexPath.section
        {
            toIndex += fetchedResultsController.sections![sectionIndex].numberOfObjects
            //print ("toIndex ",toIndex)
            if sectionIndex == fromIndexPath.section
            {
                toIndex -= 1 // Remember, controller still thinks this item is in the old section
                //print ("-= toIndex",toIndex)
            }
        }
// put the item back into he array at new position
         toDoData.insert(item, at: toIndex)

Note: toDoData is my array of records from the fetchedResultsController

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM