Search code examples
iosuitableviewcore-datansfetchedresultscontrollerswift4

Reorder TableView using fetchedResultsController using Swift 4


What I am trying to do is reordering my tableview cell. My tableview has sections: the data model the simple : list of product and each product has a category.

I am using UITableViewDropDelegate,UITableViewDragDelegate because I don't like the editing mode in tableview (the red button drive me crazy):

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


    var objects = self.fetchedResultsController.fetchedObjects! as [TblProduits]
    self.fetchedResultsController.delegate = nil

    //get the destination section
    let myDestSection = objects[destinationIndexPath.row]

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

    //updating product categorie with the destination categorie before inserting 
    object.categorie = myDestSection.categorie

    objects.insert(object, at: destinationIndexPath.row)


    fctSaveListDesProduits()
    self.fetchedResultsController.delegate = self
  } 

My app crash on this function when I try to drop a product in another section with this error:

due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (5), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (1 moved in, 0 moved out).'

How can I resolve this?

Updated

Here is the full code:

 @IBOutlet var TblView_Produit: UITableView!

let Mycontext =  (UIApplication.shared.delegate   as! AppDelegate).persistentContainer.viewContext
let Myrequest : NSFetchRequest<TblProduits> = TblProduits.fetchRequest()
var fetchedResultsController : NSFetchedResultsController<TblProduits>!
var managedObjectContext: NSManagedObjectContext? = nil

override func viewDidLoad() {
    super.viewDidLoad()

     TblView_Produit.dragDelegate = self
    TblView_Produit.dropDelegate = self
   TblView_Produit.dragInteractionEnabled = true

    // Load Data
    Fct_loadListDesProduits()
    fetchedResultsController.delegate = self

}

func Fct_SaveListDesProduits() {
    do {
        try Mycontext.save()
        //    print ("saved ligne \(i)")
    } catch {
        debugPrint ("there is an error  \(error.localizedDescription)")
    }
}

  func Fct_loadListDesProduits () {



    let MySortDescriptor = NSSortDescriptor(key: #keyPath(TblProduits.categorie.categorie_Name), ascending: true)
    Myrequest.sortDescriptors = [MySortDescriptor]
    fetchedResultsController = NSFetchedResultsController(fetchRequest: Myrequest, managedObjectContext: Mycontext, sectionNameKeyPath:  #keyPath(TblProduits.categorie.categorie_Name), cacheName: nil)

    do {
        try  fetchedResultsController.performFetch()

    } catch {
        debugPrint ("there is an error  \(error.localizedDescription)")

    }
}


   func numberOfSections(in tableView: UITableView) -> Int {
    guard let sections = self.fetchedResultsController.sections else {
        return 0
    }

    return sections.count
}

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    guard let sections = self.fetchedResultsController.sections else {
        return 0
    }

    return sections[section].numberOfObjects
}

  func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    guard let sections = self.fetchedResultsController.sections else {
        return ""
    }

    return sections[section].name
}


// MARK: - FetchedResultsController Delegate

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {

    switch(type) {
    case .insert:
        if  let Myindexpath = newIndexPath {

            self.TblView_Produit.insertRows(at: [Myindexpath], with: .left)

        }
        break;

    case .delete:
        if let MyindexPath = indexPath {
            TblView_Produit.deleteRows(at: [MyindexPath], with: .fade)
        }
        break;

    case .move:


        break;
    case .update:


        break;
    }



}


func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    TblView_Produit.endUpdates()
}

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    TblView_Produit.beginUpdates()
}



func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! ProduitTableViewCell
    if let ProduitName = self.fetchedResultsController.object(at: indexPath).produit_name { //ProductTable[indexPath.row].produit_name{


        cell.SetListeDesProduits(ProduitName: ProduitName)
    }
    //    cell.isEditing =  self.tableView(tableView, canMoveRowAt: indexPath)
    return cell
}


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


var objects = self.fetchedResultsController.fetchedObjects! as [TblProduits]
self.fetchedResultsController.delegate = nil

//get the destination section
let myDestSection = objects[destinationIndexPath.section]

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

//updating product categorie with the destination categorie before inserting 
object.categorie = myDestSection.categorie

objects.insert(object, at: destinationIndexPath.row)


fctSaveListDesProduits()
self.fetchedResultsController.delegate = self
  } 

Update

Logging numberOfrowinsection:

I have only three sections and each section has only one item

section :  2
sections[section].numberOfObjects: 1
section :  0
sections[section].numberOfObjects: 1
section :  1
sections[section].numberOfObjects: 1
section :  2
sections[section].numberOfObjects: 1
section :  0
sections[section].numberOfObjects: 1
section :  1
sections[section].numberOfObjects: 1
section :  2
sections[section].numberOfObjects: 1
section :  0
sections[section].numberOfObjects: 1
section :  1
sections[section].numberOfObjects: 1
section :  2
sections[section].numberOfObjects: 1
section :  0
sections[section].numberOfObjects: 1
section :  1
sections[section].numberOfObjects: 1

When I do the drag drop from the item from the second section to the first

section 
section :  0
sections[section].numberOfObjects: 1
section :  1
sections[section].numberOfObjects: 1
section :  2
sections[section].numberOfObjects: 1

018-01-26 19:56:05.492956+0100 ShopTogether[8815:1168659] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3698.33.6/UITableView.m:2011
2018-01-26 19:56:05.513608+0100 ShopTogether[8815:1168659] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0.  The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (1 moved in, 0 moved out).'

// Update Fetchcontroller

I think that the problem is when I display the sections

In fact when I put nil in sectionNameKeyPath: nil ==> my table view has no sections any more and I have no crash any more but I want sections in my table view.

 fetchedResultsController = 
   NSFetchedResultsController(fetchRequest: Myrequest, managedObjectContext: Mycontext,
   sectionNameKeyPath: nil, cacheName: nil)

I am still searching for an answer, but I hope that drag and drop between different sections is allowed in tableview.


Solution

  • I am not sure this will be the cleanest way but I solved my problem by regenerating the TableView and inserting object after updating the category.

      func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    
        self.fetchedResultsController.delegate = nil
        var objects = self.fetchedResultsController.fetchedObjects
        let objectSource = fetchedResultsController.object(at: IndexPath(row: sourceIndexPath.row, section: sourceIndexPath.section))
        let MyCategorie = fetchedResultsController.object(at: IndexPath(row: 0, section: destinationIndexPath.section)).categorie
        objectSource.categorie = MyCategorie
        objects?.remove(at: sourceIndexPath.row)
        objects?.insert(objectSource, at: destinationIndexPath.row)
        Fct_SaveListDesProduits()
        self.fetchedResultsController.delegate = self
        // REGENRATING TABLEVIEW 
        Fct_loadListDesProduits()
     }