添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

I have 2 ViewControllers:

VC1 populates its tableView based on CoreData attribute isPicked, which is bool and show only items with true state. VC2 is a second Modal (not Full Screen) View Controller which allow user to change the state of isPicked attribute: check and uncheck item (make it true or false). The idea is user picks needed items end dismiss the VC2 and the items should appear in VC1.

I have implemented NSFetchedResultsController to both VC1 and VC2. And as soon as I press on first item (i.e. change isPicked state to true) I receive the error from VC1:

[error] fault: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) 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 (0 moved in, 0 moved out).

Here is how I change a state of item in VC2 (true\false):

    guard let currencyArray = fetchedResultsController.fetchedObjects else { return }
    let cell = tableView.cellForRow(at: indexPath) as! PickCurrencyTableViewCell
    for currency in currencyArray {
        if currency.shortName == cell.shortName.text {
           currency.isPicked = !currency.isPicked
            coreDataManager.save()

Here is my VC1 NSFetchedResultsController implementation:

    super.viewDidLoad()
    setupFetchedResultsController()
func setupFetchedResultsController() {
    let predicate = NSPredicate(format: "isForConverter == YES")
    fetchedResultsController = createCurrencyFetchedResultsController(and: predicate)
    fetchedResultsController.delegate = self
    try? fetchedResultsController.performFetch()
func createCurrencyFetchedResultsController(with request: NSFetchRequest<Currency> = Currency.fetchRequest(), and predicate: NSPredicate? = nil, sortDescriptor: [NSSortDescriptor] = [NSSortDescriptor(key: "shortName", ascending: true)]) -> NSFetchedResultsController<Currency> {
    request.predicate = predicate
    request.sortDescriptors = sortDescriptor
    return NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)

Here is my VC1 NSFetchedResultsController delegate:

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.beginUpdates()
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.endUpdates()
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    if let indexPath = indexPath, let newIndexPath = newIndexPath {
        switch type {
        case .update:
            tableView.reloadRows(at: [indexPath], with: .none)
        case .move:
            tableView.moveRow(at: indexPath, to: newIndexPath)
        case .delete:
            tableView.deleteRows(at: [indexPath], with: .none)
        case .insert:
            tableView.insertRows(at: [indexPath], with: .none)
        default:
            tableView.reloadData()

And finally my VC1 Table View methods:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let currency = fetchedResultsController.sections![section]
        return currency.numberOfObjects
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "converterCell", for: indexPath) as! ConverterTableViewCell
        let currency = fetchedResultsController.object(at: indexPath)
        cell.shortName.text = currency.shortName
        cell.fullName.text = currency.fullName
        return cell

When I reload the app, picked item shows itself in VC1 (which caused crash). But every change in VC2 crashes the app again with the same error. I don't have any methods to delete items in VC1 or so. I need that VC1 tableView just show or hide items according to isPicked state made from VC2.

What I missed in my code?

The index path of the changed object (this value is nil for insertions).

So, you should change the code like this:

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
      switch type {
            case .update:
   	            if let indexPath = indexPath {
                   tableView.reloadRows(at: [indexPath], with: .none)
            case .move:
               if let indexPath = indexPath {
                  tableView.moveRow(at: indexPath, to: newIndexPath)
           case .delete:
                if let indexPath = indexPath {
                   tableView.deleteRows(at: [indexPath], with: .none)
                 tableView.reloadData()	// << May be needed
           case .insert:
                if let newIndexPath = newIndexPath {
                   tableView.insertRows(at: [newIndexPath], with: .none)
                   tableView.reloadData()	// << May be needed
           default:
                tableView.reloadData()
            

I did not dig into your code, but the cause is in numberOfRowsInSection in VC1:

Probably, you delete / add row in VC1 tableView before or without datasource being updated.

Could you add a print to check what the number is ?

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let currency = fetchedResultsController.sections![section]
        print(currency.numberOfObjects)
        return currency.numberOfObjects
            

What is the log ? Is it this print: print(currency.numberOfObjects)

So, why 13 or 14 values printed ?

Which func is called when you click a cell in VC2 ? Is it

        case .insert:
            tableView.insertRows(at: [indexPath], with: .none)

If so, you insert a row, but it seems you don't update the dataSource before .

I notice an important point:

  • At start, there are 13 values
  • when you select a country, then you add one (14 values)

Is it what you expect ?

So, why 13 or 14 values printed ?

Can't understand too. At the start the table is empty... Why there is always 13 numbers and not 1,5,6 etc...

Which func is called when you click a cell in VC2 ?

    let cell = tableView.cellForRow(at: indexPath) as! PickCurrencyTableViewCell
    for currency in currencyArray {
        if currency.shortName == cell.shortName.text {
           currency.isPicked = !currency.isPicked
            coreDataManager.save()

There on VC2 it sorts the array of items and if shortName (EUR) equals to the shortName of pickedCell then it change it isPicked CoreData attribute from false to true. And if I was able to click again then from true to false. And save it to database after. The NSFetchedResultController is setup to filter and show in VC1 tableView only the state isPicked = true.

Is it what you expect ?

Not quite sure, because I expect behaviour like I pressed on cell in VC2 and VC1 should load that currency I picked based on isPicked state...

My question was : what is the print statement that is logged ? Is it print(currency.numberOfObjects)?

Could you show the complete code for VC1 and VC2 ? We cannot really understand with small pieces of code.

Database already filled with data. I receive 33 objects and they all successfully loads with NSFetchedResultsController in VC2

Could you tell which case is triggered when you tap on a row in

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        if let indexPath = indexPath, let newIndexPath = newIndexPath {
            switch type {
            case .update:
                tableView.reloadRows(at: [indexPath], with: .none)
            case .move:
                tableView.moveRow(at: indexPath, to: newIndexPath)
            case .delete:
                tableView.deleteRows(at: [indexPath], with: .none)
            case .insert:
                tableView.insertRows(at: [indexPath], with: .none)
            default:
                tableView.reloadData()

When you deleteRow, you should reload data:

        if let indexPath = indexPath, let newIndexPath = newIndexPath {
            switch type {
            case .update:
                tableView.reloadRows(at: [indexPath], with: .none)
            case .move:
                tableView.moveRow(at: indexPath, to: newIndexPath)
            case .delete:
                tableView.deleteRows(at: [indexPath], with: .none)
                tableView.reloadData()
            case .insert:
                tableView.insertRows(at: [indexPath], with: .none)
                tableView.reloadData()
            default:
                tableView.reloadData()
            

So add a print before the switch, even before the if let, to see if method is called. And whether if let are successful. Why do you need to test newIndexPath for nil, except for .move case ?

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        print(#function, indexPath, newIndexPath)
        if let indexPath = indexPath, let newIndexPath = newIndexPath {

The index path of the changed object (this value is nil for insertions).

So, you should change the code like this:

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
      switch type {
            case .update:
   	            if let indexPath = indexPath {
                   tableView.reloadRows(at: [indexPath], with: .none)
            case .move:
               if let indexPath = indexPath {
                  tableView.moveRow(at: indexPath, to: newIndexPath)
           case .delete:
                if let indexPath = indexPath {
                   tableView.deleteRows(at: [indexPath], with: .none)
                 tableView.reloadData()	// << May be needed
           case .insert:
                if let newIndexPath = newIndexPath {
                   tableView.insertRows(at: [newIndexPath], with: .none)
                   tableView.reloadData()	// << May be needed
           default:
                tableView.reloadData()
        
        
This site contains user submitted content, comments and opinions and is for informational purposes only. Apple disclaims any and all liability for the acts, omissions and conduct of any third parties in connection with or related to your use of the site. All postings and use of the content on this site are subject to the Apple Developer Forums Participation Agreement.
  • Forums
  • Terms of Use Privacy Policy License Agreements