William Chiang
William Chiang

Reputation: 85

NSFetchedResultsController issue: index 1 beyond bounds, managing custom table view cells

I am stumped with implementing the NSFetchedResultsController as I keep getting errors. I am trying to connect Core Data with the UITableView, and I am not getting to the next step by having UITableView draw out the correct number of rows according to the number of records from Core Data. I get this error message:

*** Terminating app
due to uncaught exception 'NSRangeException', reason: '*** 
[__NSSingleObjectArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]'

Here's my code below:

import UIKit
import CoreData

class CustomerProfileViewController: UITableViewController, NSFetchedResultsControllerDelegate {

    var coreDataStack: CoreDataStack!

    var itemList: [NSManagedObject] = [] // I hope to have the fetch results controller manage this object

    // The data model contains one entity "Item" with one attribute "name"

    lazy var fetchResultsControllerItem: NSFetchedResultsController<Item> = {

        // Initialize Fetch Request
        let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()

        // Configure Fetch Request
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]

        // Initialize Fetch Results Controller
        let fetchResultsControllerItem = 
            NSFetchedResultsController(fetchRequest: fetchRequest,
            managedObjectContext: self.coreDataStack.managedContext,
            sectionNameKeyPath: nil,
            cacheName: nil)
        fetchResultsControllerItem.delegate = self
        return fetchResultsControllerItem
    }()

    override func viewDidLoad() {

        super.viewDidLoad()

        do {
            try fetchResultsControllerItem.performFetch()
            print("Fetch worked! 🐺")
        } catch let error as NSError {
            print("Could not save. \(error), \(error.userInfo)")
        }

        let cellNib = UINib(nibName: "SummaryCell", bundle: nil)
        tableView.register(cellNib, forCellReuseIdentifier: "SummaryCell")
        let cellNib = UINib(nibName: "ListingsCell", bundle: nil)
        tableView.register(cellNib, forCellReuseIdentifier: "ListingsCell")
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 2 // Corresponds to the custom table view cells as Nib files
    }

    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if section == 0 {
            return "SUMMARY"
        } else if section == 1 {
            return "ITEM LISTINGS"
        } else {
            return ""
        }
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if section == 0 {
            return 1
        } else if section == 1 {

/* Adding the "guard let sections" to the "return
sectionInfo.numberOfObject" below causes an error: *** Terminating app
due to uncaught exception 'NSRangeException', reason: '*** 
[__NSSingleObjectArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]' */
enter code here
            guard let sections = fetchResultsControllerItem.sections else {
                return 0
            }
            let sectionInfo = sections[section]
            return sectionInfo.numberOfObjects

        } else {
            return 1
        }
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        if indexPath.section == 0 {
            let cell = tableView.dequeueReusableCell(withIdentifier: "SummaryCell", for: indexPath) as! SummaryCell
            cell.totalNumberOfItems?.text = "\(itemList.count)"
            return cell
        } else if indexPath.section == 1 {

            let cell = tableView.dequeueReusableCell(withIdentifier: "ListingsCell", for: indexPath) as! ListingsCell
            let listItem = fetchResultsControllerItem.object(at: indexPath)
            cell.itemName?.text = listItem.name
            return cell
        }
        return UITableViewCell()
    }
}

Below displays the file containing the CoreDataStack implementation:

import Foundation
import CoreData

class CoreDataStack {

    private let modelName: String

    init(modelName: String) {
        self.modelName = modelName
    }

    lazy var managedContext: NSManagedObjectContext = {
        return self.storeContainer.viewContext
    }()

    private lazy var storeContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: self.modelName)
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                print("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    func saveContext() {
        guard managedContext.hasChanges else { return }

        do {
            try managedContext.save()
        } catch let error as NSError {
            print("Unresolved error \(error), \(error.userInfo)")
        }
    }
}

I did use Liya to confirm that there are Items with names on record after examining the sqlite file. To whomever having any tips or ideas, I would appreciate any help. Thanks.

Upvotes: 2

Views: 477

Answers (1)

pbasdf
pbasdf

Reputation: 21536

The problem is that, in the FetchedResultsController’s view if the world, there is only one section: section 0. But your tableView has two sections, and you want the FRC to provide the data for section 1. So you need to remap the FRC indexPaths to the correct section in the table view, in the tableView datasource methods. So in numberOfRowsInSection, your code is currently asking the FRC for the number of objects in section 1, and the FRC throws the error because, as far as it is concerned, there is no section 1. Just replace:

        let sectionInfo = sections[section]
        return sectionInfo.numberOfObjects

with:

        let sectionInfo = sections[0]
        return sectionInfo.numberOfObjects

in that method. Then in cellForRowAt, replace this:

        let cell = tableView.dequeueReusableCell(withIdentifier: "ListingsCell", for: indexPath) as! ListingsCell
        let listItem = fetchResultsControllerItem.object(at: indexPath)
        cell.itemName?.text = listItem.name
        return cell

with this:

        let cell = tableView.dequeueReusableCell(withIdentifier: "ListingsCell", for: indexPath) as! ListingsCell
        let listItem = fetchResultsControllerItem.fetchedObjects![indexPath.row]
        cell.itemName?.text = listItem.name
        return cell

(This uses the fetchedObjects property of the FRC to avoid using the wrong section in the indexPath in object(at:)).

Upvotes: 1

Related Questions