0
votes

I keep getting this error:

2017-10-18 22:57:52.401421+0300 Expense Manager[4213:133067] * Terminating app due to uncaught exception 'NSRangeException', reason: '* -[__NSArray0 objectAtIndex:]: index 0 beyond bounds for empty NSArray'

But I have no idea what did I do wrong. The tableView works just fine when numberOfSections is 1 and they are not sorted by date, but when I try and do sections for each day, it crashes on app launch.

  func userBudgetCount(_ section: Int) -> Int {
            return fetchedResultsController.sections![section].numberOfObjects
        }

        func getUserBudgetAtIndexPath(indexPath : IndexPath) -> Budget {
            return fetchedResultsController.object(at: indexPath) as Budget
        }


        override func viewDidLoad() {
            super.viewDidLoad()
            self.hideKeyboard()

            tableView.delegate = self
            tableView.dataSource = self

            self.tableView.tableFooterView = UIView()


        }

        override func viewDidAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            fetchCoreDataObject()
            tableView.reloadData()

        }

        func fetchCoreDataObject() {
            self.fetch { (complete) in
                if complete {
                    if userBudgetCount(0) >= 1 {
                        userBudgetLabel.text = replaceLabel(number: userMoney[userMoney.count - 1].userMoney)
                        tableView.isHidden = false
                        plusButton.isHidden = false
                        moreBtn.isHidden = false
                    } else {
                        tableView.isHidden = true
                        userBudgetLabel.text = "Bugetul tau"
                        plusButton.isHidden = true
                        moreBtn.isHidden = true
                    }
                }
            }
        }


        var fetchedResultsController: NSFetchedResultsController<Budget> {
            if _fetchedResultsController != nil {
                return _fetchedResultsController!
            }

            let fetchRequest = NSFetchRequest<Budget>(entityName: "Budget")

            // Set the batch size to a suitable number.
            fetchRequest.fetchBatchSize = 20

            // Edit the sort key as appropriate.
            let sortDescriptor = NSSortDescriptor(key: "dateSubmitted" , ascending: false)

            fetchRequest.sortDescriptors = [sortDescriptor]

            // Edit the section name key path and cache name if appropriate.
            // nil for section name key path means "no sections".
            let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext!, sectionNameKeyPath: "dateSection", cacheName: nil)
            aFetchedResultsController.delegate = self
            _fetchedResultsController = aFetchedResultsController

            do {
                try _fetchedResultsController!.performFetch()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }

            return _fetchedResultsController!
        }
        var _fetchedResultsController: NSFetchedResultsController<Budget>? = nil

        func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
            tableView.reloadData()
        }
     func numberOfSections(in tableView: UITableView) -> Int {
            return fetchedResultsController.sections!.count
        }

        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "expenseCell") as? ExpenseCell else { return UITableViewCell() }
            let budget = getUserBudgetAtIndexPath(indexPath: indexPath)
            cell.delegate = self
            cell.configureCell(budget: budget)
            return cell
        }

        func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
            return true
        }

        func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
            return UITableViewCellEditingStyle.none
        }

        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return userBudgetCount(section)
        }

        func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
            return "Section: \(section)"
        }
4
Which line is it crashing? As mentioned in error, you have an empty array and you are trying to access its first index. - adev
I don't know which line is crashing, it takes me to AppDelegate when it crashes. Doesn't say the line - AndreiVataselu
Add the exception breakpoint - vadian

4 Answers

0
votes

func numberOfSections(in tableView: UITableView) -> Int { guard let count = fetchedResultsController.sections?.count else {return 1} return count } Try to safe unwrap:

0
votes

When your view first loads the table view data source methods start getting called before the results controller has acquired any data. As a result anywhere you are force unwrapping something you are likely to get a crash.

To fix this look through every spot you do a forced unwrap (everywhere you use !) and do something more like this:

func numberOfSections(in tableView: UITableView) -> Int {
    return fetchedResultsController.sections?.count ?? 0
}

That will make sure something gets returned without the risk of a crash. In some places you may have a more challenging time figuring out what the default return value should be, but it should be doable.

The ?? operator is called the nil-coalescing operator. The syntax works like below:

let foo = someOptionalThing ?? defaultThing

If someOptionalThing has a value it gets assigned to foo otherwise defaultThing gets assigned to foo.

Update based on your comments:

Looks like you call return fetchedResultsController.object(at: indexPath) as Budget the results controller has no objects yet which is what I was expecting.

func getUserBudgetAtIndexPath(indexPath : IndexPath) -> Budget {
    // Make sure the section exists
    guard fetchedResultsController.sections?.count > indexPath.section else { return Budget() }
    // Make sure the section has enough objects
    guard fetchedResultsController.sections[indexPath.section].objects?.count > indexPath.row else { return Budget() }
    // We made it past both guards, must be safe
    return fetchedResultsController.object(at: indexPath) as Budget
}
0
votes

I solved it!

The problem was not in the tableView populating functions, but in the function I try to check if there is an element in my entity so I can make my tableView visible

In this code

  func fetchCoreDataObject() {
            self.fetch { (complete) in
                if complete {
                    if userBudgetCount(0) >= 1 {
                        userBudgetLabel.text = replaceLabel(number: userMoney[userMoney.count - 1].userMoney)
                        tableView.isHidden = false
                        plusButton.isHidden = false
                        moreBtn.isHidden = false
                    } else {
                        tableView.isHidden = true
                        userBudgetLabel.text = "Bugetul tau"
                        plusButton.isHidden = true
                        moreBtn.isHidden = true
                    }
                }
            }

I try to check userBudgetCount(0) which is out of bounds because there is NO section 0 at the moment I try to check if it exists. So what I needed was to check if my fetchedResultsController has any objects or not, so I replaced the if userBudgetCount(0) >= 1 with if ((fetchedResultsController.fetchedObjects?.count)! > 0) and it works fine now. Thank you everyone for answering!

0
votes

Sorry. Actually problem is in userBudgetCount method. This method is called with 0 parameter Value from fetchCoreDataObject method which is throwing exception because sections array is empty initially.

Replace uncommentthis function with below lines of code. Note these lines of code is not compiled yet.

func userBudgetCount(_ section: Int) -> Int{
 guard let sections = fetchedResultsController.sections           else {
     fatalError("No sections in   fetchedResultsController")
  // comment out the above line of  and  rerun 0 if don’t want to catch  exception 
  // rerun 0;
  }
    let sectionInfo = sections[section]
  return sectionInfo.numberOfObjects
}

EDIT After compiling on my side, I've added updated code at below. Please use it instead of above.

func userBudgetCount(_ section: Int) -> Int{
    guard let sections = fetchedResultsController.sections else {
        return 0;
    }

    if sections.count > section{
        let sectionInfo = sections[section]
        return sectionInfo.numberOfObjects
    }

    return 0;
}