Search code examples
iosuitableviewcore-dataswift3nsfetchedresultscontroller

NSFetchedResultsController adds item in wrong indexPath


I wrote simple counters app. It uses CoreData to store data. In model I have one entity named "Counter" with attributes "value" (Int64) and "position"(Int16). Second attribute is position in tableView. User can add, delete or increase counter. I also use NSFetchedResultsController to populate my tableView. App works, but when I, for example, add 5 counters, change their values, delete second and third values and add new one, the counter will be inserted not in the end of the tableView (here's the video https://youtu.be/XXyuEaobd3c). What's wrong with my code?

import UIKit
import CoreData

class TableViewController: UITableViewController, NSFetchedResultsControllerDelegate {

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

var frc: NSFetchedResultsController<Counter> = {
    let fetchRequest: NSFetchRequest<Counter> = Counter.fetchRequest()
    let sortDescriptor = NSSortDescriptor(key: "position", ascending: true)
    fetchRequest.sortDescriptors = [sortDescriptor]
    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
    do {
        try frc.performFetch()
    } catch {
        print(error)
    }
    return frc
}()


@IBAction func addCounter(_ sender: UIBarButtonItem) {
    let counter = Counter(context: context)
    counter.value = 0
    counter.position = Int16(frc.fetchedObjects!.count)
    do {
        try context.save()
    } catch {
        print(error)
    }

}

@IBAction func longPressed(_ sender: UILongPressGestureRecognizer) {
    if sender.state == UIGestureRecognizerState.began {
        let touchPoint = sender.location(in: self.view)
        if let indexPath = tableView.indexPathForRow(at: touchPoint) {
            // your code here, get the row for the indexPath or do whatever you want
            let counter = frc.object(at: indexPath)
            counter.value = 0

            do {
                try context.save()
            } catch {
                print(error)
            }
        }
    }

}


override func viewDidLoad() {
    super.viewDidLoad()

    frc.delegate = self
}


// MARK: - Table view data source

override func numberOfSections(in tableView: UITableView) -> Int {
    guard let sections = frc.sections?.count else {return 0}
    return sections
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    guard let sections = frc.sections else {return 0}
    return sections[section].numberOfObjects
}


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    let counter = frc.object(at: indexPath)
    cell.textLabel!.text! = String(counter.value)

    return cell
}

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

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch type {
    case .insert:
        guard let indexPath = newIndexPath else {return}
        tableView.insertRows(at: [indexPath], with: .fade)
    case .delete:
        guard let indexPath = indexPath else {return}
        tableView.deleteRows(at: [indexPath], with: .fade)
    case .update:
        guard let indexPath = indexPath else {return}
        let cell = tableView.cellForRow(at: indexPath)
        let counter = frc.object(at: indexPath)
        cell?.textLabel?.text = String(counter.value)
    default:
        break
    }
}

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

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let counter = frc.object(at: indexPath)
    counter.value += Int64(1)

    do {try context.save()}
    catch {print(error)}

    tableView.deselectRow(at: indexPath, animated: true)
}

   // Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
            let counter = frc.object(at: indexPath)
            context.delete(counter)

        do {
            try context.save()
        } catch {
            print(error)
        }
    }
}

}


Solution

  • You are adding the position in wrong way. Let suppose you are having

    A1, A2, A3, A4, A5

    then deleted A2, A3

    after that you are having A1, A4, A5

    So adding new one is B3, as count now is 3 A1, B3, A4, A5

    Adding another row becomes B4, so it can take any place either before A4 or after that.

    This order is due to the sort order on position.

    You can solve this issue, by getting last fetched object and increment the position and result then will be:

    A1, A4, A5, B6, B7