Save new order to core data after the using the tableView:moveRowAtIndexPath:toIndexPath: method
Asked Answered
W

4

6

I have a tableView in a Swift iOS app that allows the user to re-order the rows. An Edit button is tapped, rows can be re-ordered or deleted, and the Edit button (which now reads Done) is re-tapped to complete the process.

If I simply use the tableView:moveRowAtIndexPath:toIndexPath: method, it works as described above and the table shows the re-ordered rows correctly when Done is tapped. But it doesn't remember the new order when returning to the table, so I added a new "position" variable (Int) in core data and I have made my table sort itself based on that.

My problem is that after a user has moved a row and clicks Done, the table immediately re-orders itself back to how it was before. So it is using the old (original) position variables in core data, before my code can properly capture the new order and re-write it to core data.

Is there a way to make this new order get captured when Done is clicked before the table reloads?

Here is my code that handles the move:

func tableView(tableView: UITableView!, moveRowAtIndexPath sourceIndexPath: NSIndexPath!, toIndexPath destinationIndexPath: NSIndexPath!) {
}

And here is my code that is called when Done is pressed. I go through each row of my table and re-assign the person's position variable in core data based on it's new row:

for currentIteration in 0..<tableView.numberOfRowsInSection(0) {
    var indexPathForCurrentIteration = NSIndexPath(forRow: currentIteration, inSection: 0)
    let personAtCurrentIndexIteration = fetchedResultsController.objectAtIndexPath(indexPathForCurrentIteration) as PersonModel
    personAtCurrentIndexIteration.position = indexPathForCurrentIteration.row
    println("\(personAtCurrentIndexIteration.name) has a position value of \(personAtCurrentIndexIteration.position)")
    (UIApplication.sharedApplication().delegate as AppDelegate).saveContext()
        tableView.reloadData();
}

The above is called when the Done button is pressed, but I also tried calling it within the moveRowAtIndexPath method without success (same result - it reloads the old order of the table when Done is pressed). I have also tried commenting out reloadData without success.

I think my problem might lie in the fact that the above is not capturing the new row order, but instead still gets the old row order. If that is the case, then I don't know the simplest way to make it "wait" to get the new row order.

A longer workaround that I started considering would be to capture the moved row's initial and final row value, and based on that update the remaining rows' values accordingly. Some of that code is below but I stopped since I figured there is likely a smarter way.

if((sourceIndexPath.row > indexPathForCurrentIteration.row) && (destinationIndexPath.row > indexPathForCurrentIteration.row)) {
    //if the moved row was ahead of this row before AND after the move, do nothing (don't change this row's position value)
    //println("\(personAtCurrentIndexIteration.name)'s position was unchnaged")
} else if((sourceIndexPath.row > indexPathForCurrentIteration.row) && (destinationIndexPath.row < indexPathForCurrentIteration.row)) {
    //if the moved row was ahead of this row before BUT behind it after the move, then this row's position value needs to be bumped up one number
    personAtCurrentIndexIteration.position = indexPathForCurrentIteration.row+1
//Etc...

Thanks in advance for any help you can provide.

Writhe answered 9/2, 2015 at 15:19 Comment(0)
B
8
  1. Add to CoreData "orderPosition" type Int attribute and add orderPosition to your Model file.

  2. When you add some data to your CoreData, add also orderPosition value

      let appDel: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
      let contxt: NSManagedObjectContext = appDel.managedObjectContext!
      let en = NSEntityDescription.entityForName("MyCoreData", inManagedObjectContext: contxt)
      let freq = NSFetchRequest(entityName: "MyCoreData")
      var MyData = contxt.executeFetchRequest(freq, error: nil)!
      var newItem = Model(entity: en!, insertIntoManagedObjectContext: contxt)
      var newOrderPosition = MyData.count
    
    
      //here you add some data to your CoreData
      //and also you must add new order position
      newItem.orderPosition = orderPosition
    
  3. In your TableView you must create a function that will sort an array that you will display in your TableView. Something like this:

        func RefreshCoredata()
    {
        let appDel: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
        let freq = NSFetchRequest(entityName: "MyCoreData")
        let contxt: NSManagedObjectContext = appDel.managedObjectContext!
    
        let sortDescriptor = NSSortDescriptor(key: "orderPosition", ascending: true)
        freq.sortDescriptors = [sortDescriptor]
        MyData = contxt.executeFetchRequest(freq, error: nil)!
    
    } 
    
  4. Remember, you must always sort your array before send him to table!

  5. What you must do when you want to reordering your table rows? :

    override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
     }
    
    override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
    
    let appDel: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
    let contxt: NSManagedObjectContext = appDel.managedObjectContext!
    
    RefreshCoredata()
    
    
    if fromIndexPath.row > toIndexPath.row
    {
        for i in toIndexPath.row..<fromIndexPath.row
    
        {
            MyData[i].setValue(i+1, forKey: "orderPosition")
    
    
        }
    
        MyData[fromIndexPath.row].setValue(toIndexPath.row, forKey: "orderPosition")
    }
    if fromIndexPath.row < toIndexPath.row
    {
    
        for i in fromIndexPath.row + 1...toIndexPath.row
    
        {
            MyData[i].setValue(i-1, forKey: "orderPosition")
    
    
        }
    
        MyData[fromIndexPath.row].setValue(toIndexPath.row, forKey: "orderPosition")
    
    
    
    }
    
    contxt.save(nil)
    
    RefreshCoredata()
    
    }
    

This need to reset ordering position in your CoreData

  1. What you must do when you want to delete some table rows?

    override func tableView(tableView: UITableView, commitEditingStyle editingStyle:
    UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath)
    {
    if editingStyle == .Delete {
    let appDel: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
    let contxt: NSManagedObjectContext = appDel.managedObjectContext!
    RefreshCoredata()
    contxt.deleteObject(MyData[indexPath.row] as NSManagedObject)
    for i in indexPath.row + 1..<MyData.count
            {
                MyData[i].setValue(i-1, forKey: "orderPosition")
            }
    RefreshCoredata()
    
     tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
     contxt.save(nil)
    
     }
    

That's all!

Branchiopod answered 7/3, 2015 at 23:18 Comment(0)
G
3

I recently solved the problem in an app that I made that stores a list of Places in Core Data, and these places are transferred to an array Places[] to be shown in the table view.

As mentioned above, you need to create an 'orderID' attribute in your entity.

When you rearrange a row in moveRowAt, make sure you reload the table data.

override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    let movedObject = places[sourceIndexPath.row]
    places.remove(at: sourceIndexPath.row)
    places.insert(movedObject, at: destinationIndexPath.row)

    tableView.reloadData()
}

This will reload every cell in the table. Therefore, when you are setting the contents of each cell in the cellForRowAt function, then you can set the orderID using the

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "Cell")

    places[indexPath.row].setValue(indexPath.row, forKey: "orderID")
    cell.textLabel?.text = places[indexPath.row].value(forKey: "name") as? String

    return cell

}

Possibly inefficient reloading the data after every move but for small data sets this is fine. Also doesn't work if you need some kind of other ordering as this will just assign each value to the index of the cell where it has been placed currently.

Glottochronology answered 21/11, 2017 at 11:57 Comment(1)
Thank you. I think it's the most simpliest solution for saving reordering cells with core data.Milly
C
1

I can't help with fixing your problem since I need more code for that. The way I do it in my Core Data projects is to maintain a local buffer which I use to do all my manipulations and update Core Data when it is appropriate. For example:

  1. I update the local buffer from the Core Data in viewDidLoad of the initial controller
  2. I work mostly in the local buffer until I do something like moving and deleting rows. At that point I update the Core Data. This is primarily a one way operation since my local buffer is up-to-date. It can be made efficient by a good algorithm.
  3. You still need to maintain the row-order integer because Core Data does not guarantee maintaining a specific order.
  4. I also update Core Data in the didReceiveMemoryWarning in case I get a warning.

What are the risks?

If your program is terminated as a result of didReceiveMemoryWarning there is a risk that your data can be old. If you have a large table and the user has juggled it a lot this approach may not be suitable because of delays encountered during a big update. I am sure there are others.

Crackle answered 9/2, 2015 at 16:11 Comment(1)
Thanks for quick the reply Syed, much appreciated. So you build a local buffer on initial view of the controller, use that to show the rows in the correct order, and then update that buffer when edits are made. I will try this and hopefully it will allow my new row order to be shown when Done is clicked (and then I will write the new order to Core Data afterwards).Writhe
T
0

Assuming use of core data and fetchedResultsController you also need to know that the above solutions work when there are no sections in the tableview. Once you have sections you need to be aware that the from and to row values from indexpath.row are not giving you the correct position of the row to delete & insert elsewhere. You have to work out the positions based on which section the row is in and going to:-

       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)

toDoData is the array where I have loaded the data sorted by section and order

Travis answered 13/9, 2018 at 18:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.