Silver challenge - crashes when trying to implement the 'No Items' text

I only modified ItemsViewController.swift as follows:

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    
    // If the count in an itemList is 0, I want to return only 1 row for the default cell value.
    
    if section == 0 {
        if itemStore.over50Items.count == 0 {
            return 1
        } else {
            return itemStore.over50Items.count
        }
        
    } else {
        if itemStore.under50Items.count == 0 {
            return 1
        } else {
            return itemStore.under50Items.count
        }
        
    }
    
    
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   
    // better yet, get a new or recycled cell
    let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
    
    // Silver challenge: default a row to "No Items!"
    cell.textLabel?.text = "No Items!"
    
    // Set the text on the cell with the description of the item
    // that is at the nth index of items, where n = row this cell
    // will appear in on the table view
    
    if indexPath.section == 0 {
        cell.textLabel?.text = itemStore.over50Items[indexPath.row].name
        cell.detailTextLabel?.text = "$\(itemStore.over50Items[indexPath.row].valueInDollars)"
    } else  {
        cell.textLabel?.text = itemStore.under50Items[indexPath.row].name
        cell.detailTextLabel?.text = "$\(itemStore.under50Items[indexPath.row].valueInDollars)"
    }
    
    return cell

    
}

Before I modified tableView( _ tableView: numberOfRowsInSection ) -> Int, I was able to get the placeholder to display in my first section only after clicking Add for the first time. After having modified it, it crashes and the log points me to

if indexPath.section == 0 {
        cell.textLabel?.text = itemStore.over50Items[indexPath.row].name

with a “Fatal error: Index out of range” error.

I inferred from the description that I need to modify only ItemsViewController to set the placeholder cells. But I poured through the protocol documentation and didn’t find any functions that set default values.

Am I starting the right approach? If I’m not, how do I begin to identify the right approach?

In this function, you need to check to see if the container is empty before trying to get something out of it. If, for example, there are no items over 50, then trying to index into the over50Items array will cause a crash no matter what the index is. And since you modified the other function to tell the tableView that each section always has at least one row in it even when the corresponding array is empty, that becomes a real issue. The tableView is now guaranteed to call this function at least once for each section.

I’m trying to understand what you’re saying.

I tried a few things after reading your reply, but I’m not getting anywhere:

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    
    // I changed this back to its previous state
    
    if section == 0 {
        return itemStore.over50Items.count
    } else {
        return itemStore.under50Items.count
    }
    
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    // Create an instance of UITableViewCell with default appearance
    // let cell = UITableViewCell(style: .value1, reuseIdentifier: "UITableViewCell")
    
    
    
    // better yet, get a new or recycled cell
    let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
    
    // Set the text on the cell with the description of the item
    // that is at the nth index of items, where n = row this cell
    // will appear in on the table view
    
    // After the last reply, I first tried splitting my first if statement into two, checking whether each itemList is empty. If so, I set the cell text. But the text ONLY displayed after I clicked Add it added an item to the opposite section.
    if itemStore.over50Items.isEmpty && itemStore.under50Items.isEmpty {
        print ("both itemLists are empty!") // this does not display
        cell.textLabel?.text = "No Items!"
        cell.detailTextLabel?.text = ""
        return cell
    } else if indexPath.section == 0 {
            
        cell.textLabel?.text = itemStore.over50Items[indexPath.row].name
        cell.detailTextLabel?.text = "$\(itemStore.over50Items[indexPath.row].valueInDollars)"
    } else {
        cell.textLabel?.text = itemStore.under50Items[indexPath.row].name
        cell.detailTextLabel?.text = "$\(itemStore.under50Items[indexPath.row].valueInDollars)"
    }
    return cell

    
}

I don’t see anything here that would cause a crash, so if it is crashing I’m not sure why.

OTOH, if the problem is that you’re not getting the “No Items!” cell to display, I think that’s because the cellForRowAt function is only called if the tableVIew thinks there is at least one cell to display. If both sections have 0 items, it doesn’t get called.

So if you want to display “No Items!” when both containers are empty, you’re going to have to have the numberOfRowsInSection function return a value of 1 for one of the sections when both arrays are empty. It’s up to you which section you want the message to appear in.

Now, I can display ‘No Items’ within both sections by default.

I can also prevent a user from Deleting those rows. Even though the Delete button appears for these rows once I click Edit, clicking Delete doesn’t do anything.

Now it crashes when I click Add.

I know I need to delete the ‘No Items!’ row before I can add a new item from either of my Item arrays, but I’m either 1) not doing it correctly, and/or 2) not doing it in the right place.

(ItemsViewController.swift)

@IBAction func addNewItem(_ sender: UIButton) {
    // Create a new item and add it to the store
    let newItem = itemStore.createItem()
    
    /* print("Number of sections: \(tableView.numberOfSections)")
    
    for i in 0..<sections.count {
        print("Section \(i): \(sections[i])")
    }*/
    
    // Silver challenge - can I use this to designate my 'No Items!' row?
    let placeholderIndex = tableView.indexPathsForVisibleRows!
        
    
    // Figure out where that item is in the array
    if let index = itemStore.over50Items.firstIndex(of: newItem) {
        // Silver challenge - trying to delete placeholder row before adding the first item from over50Items
        tableView.deleteRows(at: placeholderIndex, with: .none)
        
        let indexPath = IndexPath(row: index, section: 0)
        
        // Insert this new row into the table
        tableView.insertRows(at: [indexPath], with: .automatic)
        
        
        print("Item Name: \(newItem.name)")
        print("Value: $\(newItem.valueInDollars)")
        // print("Section: \(newItem.overUnder50)")
        
        
    } else if let index = itemStore.under50Items.firstIndex(of: newItem) {
        
        // Silver challenge - trying to delete placeholder row before adding the first item from over50Items
        tableView.deleteRows(at: placeholderIndex, with: .none)
        
        let indexPath = IndexPath(row: index, section: 1)
        tableView.insertRows(at: [indexPath], with: .automatic)
        
        // console stuff
        print("Item Name: \(newItem.name)")
        print("Value: $\(newItem.valueInDollars)")
        
        
    }
    
    
    
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    // Create an instance of UITableViewCell with default appearance
    // let cell = UITableViewCell(style: .value1, reuseIdentifier: "UITableViewCell")
    
    
    
    // better yet, get a new or recycled cell
    let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
    
    // Set the text on the cell with the description of the item
    // that is at the nth index of items, where n = row this cell
    // will appear in on the table view
    
    // After the last reply, I first tried splitting my first if statement into two, checking whether each itemList is empty. If so, I set the cell text. But the text ONLY displayed after I clicked Add it added an item to the opposite section.
    
    if itemStore.over50Items.isEmpty {
        print ("over50Items is empty!") // this does not display
        cell.textLabel?.text = "No Items!"
        cell.detailTextLabel?.text = ""
        print(indexPath) // indexPath = [0, 0]
        return cell
    } else {
        tableView.deleteRows(at: [indexPath], with: .automatic) // not sure if this is doing anything
    }
    
    if itemStore.under50Items.isEmpty {
        print ("under50Items is empty!") // this does not display
        cell.textLabel?.text = "No Items!"
        cell.detailTextLabel?.text = ""
        print(indexPath) // indexPath = [1, 0]
        return cell
    }  else {
        tableView.deleteRows(at: [indexPath], with: .automatic) // not sure if this is doing anything
           }
        
    if indexPath.section == 0 {
        cell.textLabel?.text = itemStore.over50Items[indexPath.row].name
        cell.detailTextLabel?.text = "$\(itemStore.over50Items[indexPath.row].valueInDollars)"
        return cell
    } else {
        cell.textLabel?.text = itemStore.under50Items[indexPath.row].name
        cell.detailTextLabel?.text = "$\(itemStore.under50Items[indexPath.row].valueInDollars)"
        return cell
    }
    

    
}

// Listing 9.20 p 208 - Implementing table view row deletion
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    
    
    
    // If the table view is asking to commit a delete command...
    if editingStyle == .delete {
        
        
        // Silver challenge: apply remove/delete only when the itemList has items
        if indexPath.section == 0 && !(itemStore.over50Items.isEmpty) {
            // remove the item from the store
            itemStore.removeItem(itemStore.over50Items[indexPath.row])
            
            // Also remove that row from the table view with an animation
            tableView.deleteRows(at: [indexPath], with: .automatic)
        } else if indexPath.section == 1 && !(itemStore.under50Items.isEmpty){
            itemStore.removeItem(itemStore.under50Items[indexPath.row])
            // Also remove that row from the table view with an animation
            tableView.deleteRows(at: [indexPath], with: .automatic)
        } else {
            _ = UITableViewCell.EditingStyle.none
            // the Edit controls still appear, but nothing happens after I click Delete on the 'No Items' row.
        }
        
        
    }
}

// Silver Challenge addition to turn off editing

override func tableView(_ tableView: UITableView,
                        canEditRowAt indexPath: IndexPath) -> Bool {
    let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
    
    if cell.textLabel?.text == "No Items!" {
        return false
    } else {
        return true
    }
    // result: the Edit controls still appear, but nothing happens after I click Delete on the 'No Items' row.
}

Isn’t there a way to simply deleteRow() from the TableView where its label = ‘No Items!’?

What error message are you getting when it crashes?

LootLogger[74545:4826064] *** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Invalid update: invalid number of rows in section 0. 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 (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).’

My interpretation of this error message: the app is trying to add the first item into the first row of the section, which is already occupied by ‘No Items’.

I think what’s happening is that when you try to delete the “No Items!” cell from the table view, the number of rows doesn’t actually change. The numberOfRowsInSection function was returning 1 before you deleted the “No Items!” cell; since you deleted a row, numberOfRowsInSection should be 0 now but it isn’t, it’s still 1, so the table view is confused.

I don’t see a good way around this using a cell to display the “No Items!” message. You might have to use a section footer to display that instead, and turn the footer off when the number of rows in the section is non-zero.

Thanks - I made that work…

@IBAction func addNewItem(_ sender: UIButton) {
    
    // Listing 9.15 p 205
    // Make a new index path for the 0th section, last row
    /* this crashes the app
    let lastRow = tableView.numberOfRows(inSection: 0)
    let indexPath = IndexPath(row: lastRow, section: 0)
    */
    
    
    // Create a new item and add it to the store
    let newItem = itemStore.createItem()
    
    /* print("Number of sections: \(tableView.numberOfSections)")
    
    for i in 0..<sections.count {
        print("Section \(i): \(sections[i])")
    }*/
    
    // Silver challenge - can I use this to designate my 'No Items!' row?
    // let placeholderIndex = tableView.indexPathsForVisibleRows!
        
    
    // Figure out where that item is in the array
    if let index = itemStore.over50Items.firstIndex(of: newItem) {
        // Silver challenge - trying to delete placeholder row before adding the first item from over50Items
    
        
        let indexPath = IndexPath(row: index, section: 0)
        
        // Insert this new row into the table
        tableView.insertRows(at: [indexPath], with: .automatic)
        
        tableView.footerView(forSection: 0)?.isHidden = true // footer goes away
        
        print("Item Name: \(newItem.name)")
        print("Value: $\(newItem.valueInDollars)")
        // print("Section: \(newItem.overUnder50)")
        
        
    } else if let index = itemStore.under50Items.firstIndex(of: newItem) {
        
        // Silver challenge - trying to delete placeholder row before adding the first item from over50Items
        let indexPath = IndexPath(row: index, section: 1)
        tableView.insertRows(at: [indexPath], with: .automatic)
        
        tableView.footerView(forSection: 1)?.isHidden = true // footer goes away
        
        // console stuff
        print("Item Name: \(newItem.name)")
        print("Value: $\(newItem.valueInDollars)")
        
        
    }
    
    
    
}

// Listing 9.20 p 208 - Implementing table view row deletion
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    
    
    
    // If the table view is asking to commit a delete command...
    if editingStyle == .delete {
        
        
        // Silver challenge: apply remove/delete only when the itemList has items
        if indexPath.section == 0 {
            // remove the item from the store
            itemStore.removeItem(itemStore.over50Items[indexPath.row])
            
            // Also remove that row from the table view with an animation
            tableView.deleteRows(at: [indexPath], with: .automatic)
            
            if itemStore.over50Items.isEmpty {
                tableView.footerView(forSection: 0)?.isHidden = false
                // footer re-appears
            }
        } else {
            itemStore.removeItem(itemStore.under50Items[indexPath.row])
            // Also remove that row from the table view with an animation
            tableView.deleteRows(at: [indexPath], with: .automatic)
            
            if itemStore.under50Items.isEmpty {
            tableView.footerView(forSection: 1)?.isHidden = false
                // footer re-appears
            }
        }
        
        
    }
}

    // Create a standard footer that includes the returned text.
override func tableView(_ tableView: UITableView, titleForFooterInSection
                            section: Int) -> String? {
    
    return "No Items!"
}

Is there a way to accomplish this while using UITableCells?

I’ve been able to mostly accomplish this but adding gives me an error because of how my count functionality is done and I feel SO CLOSE but so far from the solution that lets me use UITableCells.

The footer method is easy but doesn’t look as good. Posted bits of my code that’s relevant below.

@IBAction func addNewItem(_ sender: UIButton) {
    // Create new item and add it to store
    let newItem = itemStore.createItem()
    
    // Figure out where that item is in the array
    for (sectionIndex, section) in itemStore.allItems.enumerated() {
        if let rowIndex = section.firstIndex(of: newItem) {
            let indexPath = IndexPath(row: rowIndex, section: sectionIndex)
            
            // Check if first item
            if section.count == 1 {
                print(tableView.numberOfRows(inSection: sectionIndex))
                tableView.deleteRows(at: [indexPath], with: .automatic)
            }
            tableView.insertRows(at: [indexPath], with: .automatic)
        }
    }
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let rowCount = itemStore.allItems[section].count
    if tableView.cellForRow(at: IndexPath(row: 0, section: section))?.textLabel?.text == "No Items!" {
        
    }
    print("row count check \(rowCount)")
    return rowCount > 0 ? rowCount : 1
}

@iosdev I think your numberOfRowsInSection() method is actually fine (the if statement is empty and can be removed, but the return value looks fine). Without knowing the error message, I would guess that the call to deleteRows() is putting the UITableView instance in an inconsistent state with its UITableViewDataSource. I’m working from the 6th edition of the book which says “Make it so the last row of the UITableView always has the text ‘No more items!’ Make sure this row appears regardless of the number of items in the store (including 0 items)”. I’m guessing this changed in the 7th edition? If the “No Items!” cell needs to be conditionally shown, I think there are two options:

  1. Group the calls to deleteRows() and insertRows() within a beginUpdates() / endUpdates() block (the newer API for this is performBatchUpdates()).
  2. Omit the calls to deleteRows() and insertRows() entirely and call reloadData() on the table view after updating the model (item store).

@Caps5150 I don’t know if you’re still following this thread, but the reason the edit controls were still appearing on the “No Items!” cells is because your canEditRowAt() method was always returning true. The way it’s written, it dequeues an empty table cell (rather than the cell at the specified index path) and checks if its textLabel's text is “No Items!”, which is always false (unless a previously used cell is returned and the data wasn’t cleared for some reason). The edit controls didn’t do anything, however, because the logic in your commitEditingStyle() method is correctly preventing them from doing so. As far as the crash, I think you and @iosdev are probably running into the same issue with deleteRows().

@rosesoftwareshop Took your advice on calling reloadData() except I called the reloadRows(at:with:) instead so that I could still have the option of keeping the animation effect.

Here’s my solution

import UIKit
class ItemsViewController: UITableViewController {
    
    var itemStore: ItemStore!
    
    @IBAction func addNewItem(_ sender: UIButton) {
        // Create a new item and add it to the store
        let newItem = itemStore.createItem()
        
        let section = newItem.valueMoreThan50 ? 0 : 1                              // Bronze Challenge
        
        // Figure out where that item is in the array
        if let index = itemStore.allItems[section].firstIndex(of: newItem) {        // Bronze Challenge
            let indexPath = IndexPath(row: index, section: section)                 // Bronze Challenge
            
            // Silver Challenge
            if itemStore.allItems[section].count == 1 {
                tableView.reloadRows(at: [indexPath], with: .automatic)
            } else {
                // Insert this new row into the table
                tableView.insertRows(at: [indexPath], with: .automatic)
            }
            
        }
    }
    
    @IBAction func toggleEditingMode(_ sender: UIButton) {
        // If you are currently in editing mode...
        if isEditing {
            // Change text of button to inform user of state
            sender.setTitle("Edit", for: .normal)
            
            // Turn off editing mode
            setEditing(false, animated: true)
        } else {
            // Change text of button to inform user of state
            sender.setTitle("Done", for: .normal)
            
            // Enter editing mode
            setEditing(true, animated: true)
        }
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return itemStore.allItems.count
    }
    
    // Bronze Challenge
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return itemStore.allItems[section].isEmpty ? 1 : itemStore.allItems[section].count    // Silver Challenge
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // Get a new or recycled cell
        let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
        
        // Set the text on the cell with the description of the item
        // that is at the nth index of items, where n = row this cell
        // will appear in on the table view
        let item: Item
        
        if !itemStore.allItems[indexPath.section].isEmpty {
            item = itemStore.allItems[indexPath.section][indexPath.row]    // Bronze Challenge
            cell.textLabel?.text = item.name
            cell.detailTextLabel?.text = "$\(item.valueInDollars)"
        } else {
            cell.textLabel?.text = "Section is Empty"
            cell.detailTextLabel?.text = ""
        }
        
        return cell
    }
    
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        // If the table view is asking to commit a delete command...
        if editingStyle == .delete {
            let item = itemStore.allItems[indexPath.section][indexPath.row]    // Bronze Challenge
            
            // Remove the item from the store
            itemStore.removeItem(item)
            
            // Silver Challenge
            if itemStore.allItems[indexPath.section].count == 0 {
                tableView.reloadRows(at: [indexPath], with: .automatic)
            } else {
                // Also remove that row from the table view with an animation
                tableView.deleteRows(at: [indexPath], with: .automatic)
            }
        }
    }
    
    override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {

        itemStore.moveItem(from: sourceIndexPath, to: destinationIndexPath)     // Bronze Challenge
    }
    
    // Bronze Challenge
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        switch section {
        case 0:
            return "Items worth more than $50"
        default:
            return "Items worth less than $50"
        }
    }
    
    // Bronze Challenge
    override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
        if sourceIndexPath.section == proposedDestinationIndexPath.section {
            return proposedDestinationIndexPath
        } else {
            return sourceIndexPath
        }
    }
    
    // Silver Challenge
    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return itemStore.allItems[indexPath.section].isEmpty ? false : true
    }
}
import UIKit

class ItemStore {
    
    var allItems : [[Item]] = [[],[]]
    
    @discardableResult func createItem() -> Item {
        let newItem = Item(random: true)
        
        let section = newItem.valueMoreThan50 ? 0 : 1    // Bronze Challenge
        allItems[section].append(newItem)                // Bronze Challenge
        
        return newItem
    }
    
    func removeItem(_ item: Item) {
        let section = item.valueMoreThan50 ? 0 : 1                  // Bronze Challenge
    
        if let index = allItems[section].firstIndex(of: item) {    // Bronze Challenge
            allItems[section].remove(at: index)                     // Bronze Challenge
        }
    }
    
    func moveItem(from fromIndex: IndexPath, to toIndex: IndexPath) {
        if fromIndex == toIndex {
            return
        }

        // Get reference to object being moved so you can reinsert it
        let movedItem = allItems[fromIndex.section][fromIndex.row]

        // Remove item from array
        allItems[fromIndex.section].remove(at: fromIndex.row)

        // Insert item in array at new location
        allItems[toIndex.section].insert(movedItem, at: toIndex.row)
    }

" The edit controls didn’t do anything, however, because the logic in your commitEditingStyle() method is correctly preventing them from doing so."

I agree on this, Thanks!