I'm trying to get sections to work in my NSFetchResultController (Using Swift 3). The sections are of a calculated property of a NSManagedObject
. The sections are displayed just fine. But every time I try to add a new Object
which would generate a new section the app crashes with the following error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 4. 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 (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).'
The FetchResultController part of my TableViewController looks as follows:
lazy var fetchedResultsController: NSFetchedResultsController<Package> = {
let fetchRequest = NSFetchRequest<Package>(entityName:"Package")
let sectionDescriptor = NSSortDescriptor(key: "sectionTitle", ascending: true)
let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: true)
fetchRequest.sortDescriptors = [sectionDescriptor , sortDescriptor]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: "sectionTitle", cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}()
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
managedObjectContext = appDelegate.persistentContainer.viewContext
do {
try self.fetchedResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Package")
do {
try managedObjectContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
The TableView Data Source part of my TableViewController looks as follows:
override func numberOfSections(in tableView: UITableView) -> Int {
if let sections = fetchedResultsController.sections {
return sections.count
}
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
return 1
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if let sections = fetchedResultsController.sections {
let currentSection = sections[section]
return currentSection.name
}
return nil
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier: String = "PackageTableCell"
let cell: PackageTableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as! PackageTableViewCell
let package = fetchedResultsController.object(at: indexPath)
cell.labelId.text = String(describing: package.packageId)
cell.labelTime.text = DateFormatter.localizedString(from: package.deliveryTime as! Date, dateStyle: DateFormatter.Style.none, timeStyle: DateFormatter.Style.short)
cell.labelWeight.text = String(describing: package.weight)
cell.labelBatch.text = String(describing: package.batch!.batchId)
cell.labelRecepients.text = String(describing: package.recepients)
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRow(at: indexPath as IndexPath, animated: true)
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let package: Package = fetchedResultsController.object(at: indexPath)
managedObjectContext.delete(package)
}
}
The FetchResultController Delegate part of my TableViewController looks as follows:
func controllerWillChangeContent() {
tableView.beginUpdates()
}
func controllerDidChangeContent() {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
switch type {
case .insert:
tableView.insertSections(NSIndexSet(index: sectionIndex) as IndexSet, with: .fade)
case .delete:
tableView.deleteSections(NSIndexSet(index: sectionIndex) as IndexSet, with: .fade)
case .move:
break
case .update:
break
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch (type) {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .fade)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .fade)
case .update:
let cell = tableView.cellForRow(at: indexPath!) as! PackageTableViewCell
configureCell(cell: cell, atIndexPath: indexPath! as NSIndexPath)
case .move:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
Then In my AddViewController I save the object as shown below:
@IBAction func savePressed(_ sender: UIBarButtonItem) {
if mode == .add {
let entity = NSEntityDescription.entity(forEntityName: "Package", in: self.managedObjectContext)
package = Package(entity: entity!, insertInto: self.managedObjectContext)
let packageId: Int = UserDefaults.standard.integer(forKey: "packageId")
package?.packageId = Int32(packageId)
UserDefaults.standard.set((packageId +1), forKey: "packageId")
package?.createdAt = NSDate()
package?.location = mapView.userLocation.location! as CLLocation
}
let calendar = NSCalendar.current
let selectedDate = pickerDate.date
let selectedTime = pickerTime.date
var deliveryTimeComponents = DateComponents()
deliveryTimeComponents.year = calendar.component(.year, from: selectedDate)
deliveryTimeComponents.month = calendar.component(.month, from: selectedDate)
deliveryTimeComponents.day = calendar.component(.day, from: selectedDate)
deliveryTimeComponents.hour = calendar.component(.hour, from: selectedTime)
deliveryTimeComponents.minute = calendar.component(.minute, from: selectedTime)
let deliveryTime = calendar.date(from: deliveryTimeComponents)
package?. deliveryTime = deliveryTime as NSDate?
package?.batch = batchPicker?.selectedBatch
package?.weight = Float(inputWeight.text!)!
package?.recepients = Int16(stepperRecepients.value)
package?.notes = textNotes.text!
do {
try package?.managedObjectContext?.save()
if mode == .edit {
packageTransferDelegate?. packageTransfer(transferPackage: package!)
}
dismiss(animated: true, completion: nil)
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
}
And finally my Package Class (Package+CoreDataClass)
import Foundation
import CoreData
public class Package: NSManagedObject {
var sectionTitle: String {
return DateFormatter.localizedString(from: self.deliveryTime! as Date, dateStyle: DateFormatter.Style.long, timeStyle: DateFormatter.Style.none)
}
}
Any tips on how I could resolve this problem would be very welcome. Also I'm fairly new to iOS development, so if you see something in my code which you don't like, feel free to write me about it 😉
I found the answer to my own question. I forgot to pass the NSFetchResultContoller
into the controllerWillChangeContent
and controllerDidChangeContent
functions. Which lead to the crash.
I corrected the code as follows:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
Now everything seems to work just fine.