0

I have a list of customers in a UITableView managed by an NSFetchedResultsController, class is called CustomersViewController. When I select a customer, a new view controller CustomerDetailViewController is loaded which displays their details and then a list of radiators related to them in another UITableView managed by an NSFetchedResultsController. The only editing I need on the tables is Deletion and this works fine in both tables managed by NSFetchedResultsController.

I want to be able to edit the customers details, so I have an edit button in the NavigationBar that segues to EditCustomerViewController from CustomerDetailViewController. As with previous segues the managedObjectContext and the managedObject (the selected customer) is passed through successfully and I can access all the objects values in the EditCustomerViewController, what I can't seem to do is edit them without getting these errors:

2016-02-18 12:30:08.349 Radiator Calculator[13825:2113477] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3512.29.5/UITableView.m:1720
2016-02-18 12:30:08.351 Radiator Calculator[13825:2113477] CoreData: error: 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 (2), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)

From this error I am guessing the issue lies with the NSFetchedResultsController not liking me changing the value in the EditCustomerViewController two viewcontrollers ahead of where it was instantiated. Given that there is no table in this view controller I haven't set it up.

The code for the three viewcontrollers in question are:

Code for CustomersViewController:

import UIKit
import CoreData

class CustomersViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate {

var context: NSManagedObjectContext!

@IBOutlet weak var customerList: UITableView!

var selectedCustomer: NSManagedObject!

// MARK: - viewDidLoad

override func viewDidLoad() {
    super.viewDidLoad()

    print("Customers VC")

    let appDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate

    context = appDel.managedObjectContext

    do {
        try fetchedResultsController.performFetch()
    } catch {
        print("Error occured with FRC")
    }


}

override func viewWillAppear(animated: Bool) {
    //reload todo list data array
    customerList.reloadData()

}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}


//MARK: - Table data functions
func numberOfSectionsInTableView(tableView: UITableView) -> Int {

    if let sections = fetchedResultsController.sections {
        return sections.count
    }

    return 0

}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    if let sections = fetchedResultsController.sections {
        let currentSection = sections[section]
        return currentSection.numberOfObjects
    }

    return 0

}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Customer Cell")

    let customer = fetchedResultsController.objectAtIndexPath(indexPath)

    print(customer)

    cell.textLabel?.text = customer.name

    return cell

}

func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == UITableViewCellEditingStyle.Delete {

        let customer = fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject

        context.deleteObject(customer)

        do {
            try context.save()
        } catch let error as NSError {
            print("Error saving context after delete \(error.localizedDescription)")
        }

    }
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    selectedCustomer = fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject

    self.performSegueWithIdentifier("customerDetailSegue", sender: self)

}

// MARK: - Segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    print("seg fired")
    if segue.identifier == "addCustomerSegue" {
        if let addCustomerViewController = segue.destinationViewController as? AddCustomerViewController {
            addCustomerViewController.context = context
        }
    }

    if segue.identifier == "customerDetailSegue"{
        if let customerDetailViewController = segue.destinationViewController as? CustomerDetailViewController {
            customerDetailViewController.context = context
            customerDetailViewController.customer = selectedCustomer
        }
    }

}

// set up frc
lazy var fetchedResultsController: NSFetchedResultsController = {

    let customerFetchRequest = NSFetchRequest(entityName: "Customers")
    let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
    customerFetchRequest.sortDescriptors = [sortDescriptor]

    let frc = NSFetchedResultsController(fetchRequest: customerFetchRequest, managedObjectContext: self.context, sectionNameKeyPath: nil, cacheName: nil)

    frc.delegate = self

    return frc
}()


//MARK: NSFetchedResultsControllerDelegate methods
func controllerWillChangeContent(controller: NSFetchedResultsController) {
    self.customerList.beginUpdates()
}

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    switch type{
    case NSFetchedResultsChangeType.Insert:
        //note that for insert we insert a row at _newIndexPath_
        if let insertIndexPath = newIndexPath {
            self.customerList.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
        }
    case NSFetchedResultsChangeType.Delete:
        //note that for delete we delete the row at _indexPath_
        if let deleteIndexPath = indexPath {
            self.customerList.deleteRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
        }
    case NSFetchedResultsChangeType.Update:
        //note that for update we update the row at _indexPath_
        if let updateIndexPath = indexPath {
            let cell = self.customerList.cellForRowAtIndexPath(updateIndexPath)
            let customer = fetchedResultsController.objectAtIndexPath(updateIndexPath)
            cell!.textLabel?.text = customer.name
        }
    case NSFetchedResultsChangeType.Move:
        //note that for Move we delete the row at _indexPath_
        if let deleteIndexPath = indexPath {
            self.customerList.insertRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
        }
        //note that for move we insert a row at _newIndexPath_
        if let insertIndexPath = newIndexPath {
            self.customerList.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
        }
    }
}

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    //note needed as only have one section
}

func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String) -> String? {
    return sectionName
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    self.customerList.endUpdates()
}



}

and CustomerDetailViewController

import UIKit
import CoreData

class CustomerDetailViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate {

var context: NSManagedObjectContext!
var customer: NSManagedObject!

@IBOutlet weak var customerName: UILabel!
@IBOutlet weak var street: UILabel!
@IBOutlet weak var town: UILabel!
@IBOutlet weak var postCode: UILabel!
@IBOutlet weak var radiatorList: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()

    radiatorList.allowsSelection = false

    if let name = customer.valueForKey("name") as? String {
        customerName.text = name
    }
    if let addressLine1 = customer.valueForKey("address_line_1") as? String {
        street.text = addressLine1
    }
    if let townName = customer.valueForKey("town") as? String {
        town.text = townName
    }
    if let postcode = customer.valueForKey("postcode") as? String {
        postCode.text = postcode
    }


    // set up FRC
    do {
        try fetchedResultsController.performFetch()
    } catch {
        print("Error occured with FRC")
    }



}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}


// MARK: - Table view data source

func numberOfSectionsInTableView(tableView: UITableView) -> Int {

    if let sections = fetchedResultsController.sections {
        return sections.count
    }

    return 0
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    if let sections = fetchedResultsController.sections {
        let currentSection = sections[section]
        return currentSection.numberOfObjects
    }

    return 0

}


func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Radiator Cell", forIndexPath: indexPath) as! RadiatorCell

    let radiator = fetchedResultsController.objectAtIndexPath(indexPath) as? NSManagedObject

    if let radiatorName = radiator?.valueForKey("radiatorName") as? String {
        cell.radNameLabel.text = String(radiatorName)
    }

    if let radiatorPowerWatts = radiator?.valueForKey("radiatorPowerWatts") as? Double{
        print(radiatorPowerWatts)
        cell.radPowerWattsLabel.text = "\(ceil(radiatorPowerWatts)) Watts"
        cell.radPowerBtusLabel.text = "\(ceil(radiatorPowerWatts / 0.293)) BTUs"
    }



    return cell
}

/*
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {


}
*/


func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == UITableViewCellEditingStyle.Delete {

        let radiator = fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject

        context.deleteObject(radiator)

        do {
            try context.save()
        } catch let error as NSError {
            print("Error saving context after delete \(error.localizedDescription)")
        }

    }
}


// set up frc
lazy var fetchedResultsController: NSFetchedResultsController = {

    let radiatorFetchRequest = NSFetchRequest(entityName: "Radiators")
    let sortDescriptor = NSSortDescriptor(key: "radiatorName", ascending: true)
    radiatorFetchRequest.predicate = NSPredicate(format: "customer = %@", self.customer)

    radiatorFetchRequest.sortDescriptors = [sortDescriptor]

    let frc = NSFetchedResultsController(fetchRequest: radiatorFetchRequest, managedObjectContext: self.context, sectionNameKeyPath: nil, cacheName: nil)

    frc.delegate = self

    return frc
}()


//MARK: NSFetchedResultsControllerDelegate methods
func controllerWillChangeContent(controller: NSFetchedResultsController) {
    self.radiatorList.beginUpdates()
}

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    switch type{
    case NSFetchedResultsChangeType.Insert:
        //note that for insert we insert a row at _newIndexPath_
        if let insertIndexPath = newIndexPath {
            self.radiatorList.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
        }
    case NSFetchedResultsChangeType.Delete:
        //note that for delete we delete the row at _indexPath_
        if let deleteIndexPath = indexPath {
            self.radiatorList.deleteRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
        }
    case NSFetchedResultsChangeType.Update:
        //note that for update we update the row at _indexPath_
        if let updateIndexPath = indexPath {
            let cell = self.radiatorList.cellForRowAtIndexPath(updateIndexPath)
            let radiator = fetchedResultsController.objectAtIndexPath(updateIndexPath)
            cell!.textLabel?.text = radiator.name
        }
    case NSFetchedResultsChangeType.Move:
        //note that for Move we delete the row at _indexPath_
        if let deleteIndexPath = indexPath {
            self.radiatorList.insertRowsAtIndexPaths([deleteIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
        }
        //note that for move we insert a row at _newIndexPath_
        if let insertIndexPath = newIndexPath {
            self.radiatorList.insertRowsAtIndexPaths([insertIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
        }
    }
}

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    //note needed as only have one section
}

func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String) -> String? {
    return sectionName
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    self.radiatorList.endUpdates()
}




// MARK: - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "mapSegue"{

        if let mapVC = segue.destinationViewController as? CustomerMapViewController{

            var addressString = String()

            if let addressLine1 = customer.valueForKey("address_line_1") as? String {
                let s = addressLine1
                addressString += "\(s), "
            }
            if let townName = customer.valueForKey("town") as? String {
                let t = townName
                addressString += "\(t), "
            }
            if let postcode = customer.valueForKey("postcode") as? String {
                let p = postcode
                addressString += "\(p)"
            }

            mapVC.address = addressString


        }

    }

    if segue.identifier == "editCustomerSegue" {

        if let editVC = segue.destinationViewController as? EditCustomerViewController {

            editVC.context = context
            editVC.customer = customer

        }

    }

}

}

and

import UIKit
import CoreData

class EditCustomerViewController: UIViewController {

var context: NSManagedObjectContext!
var customer: NSManagedObject!

override func viewDidLoad() {
    super.viewDidLoad()

    print("Edit customer view controller")

    if let name = customer.valueForKey("name") as? String {
        //this works
        print(name)
    }

    customer.setValue("Hardcoded name change", forKey: "name")





}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}


/*
// MARK: - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // Get the new view controller using segue.destinationViewController.
    // Pass the selected object to the new view controller.
}
*/

}

The application keeps running until I navigate back to the CustomersViewController when it finally crashes and I can briefly see that the customer name has changed in the list - it hasn't changed in the CustomerDetailViewController however.

Any help would be great, apologies for any lack of "swifty-ness" (I read that's a thing) - this is my first larger app in swift and iOS so I'm still learning as I go.

SimonBarker
  • 1,384
  • 2
  • 16
  • 31
  • I think you may be a victim of the horrible saga set out in [this question](http://stackoverflow.com/q/31383760/3985749) and the various answers to it. I would log (or use a breakpoint to step through) the `controller:didChangeObject:` in your `CustomerViewController` to see exactly what's going on. – pbasdf Feb 18 '16 at 22:52
  • Thanks for the link to the question - a saga indeed. I managed to get it working late last night - in the end I set the fetchedResultsController delegate to nil when the view disappears and then reset it to self in viewWillAppear and then reperformed the fetch request - not ideal since I had to do it in every single viewcontroller that uses an FRC but it seems stable at the moment. – SimonBarker Feb 19 '16 at 09:40

0 Answers0