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!"
}