Bronze Challenge - Generally speaking, what's the best approach to tackle Challenges throughout the book?

Here’s my approach, and here’s the end result so far:

  1. I know that I my TableView must display two sections: “More than $50” and “$50 or Less”.

  2. After I tap Add, the Item that randomly generates must go into one section or the other based on its valueInDollars.

  3. To do this, I need to modify my data source (ItemsViewController.swift per p.189) by adding required methods to conform to the UITableViewDataSource protocol, and (maybe?) another method to return the number of sections.

I’ve spent a few hours on this challenge of the last day or so, and all I can think of doing is hard-code the number of sections:

 // Bronze Challenge
override func numberOfSections(in tableView: UITableView) -> Int {
    return 2
}

The app builds, but crashes at runtime.

Scouring the inter webs for ‘recipes’ to follow haven’t been all that fruitful. And even the code samples that Apple manages to furnish aren’t completely helpful either.

I might not be in the right mindset to tackle these. But all I know is that I don’t know/can’t find the correct way to:

  1. Create and name each section (Sure I can create an array of strings with my two section titles above, but then how do I set each section to each of those values?).
  2. In which file (Item? ItemStore? ItemsViewController?) to check whether the item’s valueInDollars is > or <= $50.

Questions for those who can complete these challenges more efficiently:

  1. What’s the best way to spot hints within a Challenge description that direct you towards a correct solution?
  2. Do you find yourself consulting the chapter in the book more than Apple Developer Documentation to complete these challenges? And how does that change as one progresses from Bronze to Silver to Gold Challenges?
  3. Other than auto-complete and option-click, what do you use in Xcode to guide you towards a correct solution?
  4. What are common pitfalls you encounter as you build a solution? And what steps do you take to overcome them?
  5. As you do these Challenges, which resources do you avoid? (I find StackOverflow to be a bit overwhelming)

This is a frustrating endeavor. But I want to learn how to do this because I want to build stuff that’s useful and compelling.

But 8 chapters in, I’ve only become most proficient at re-typing code from a book, dragging and dropping items on a user interface, and read just a little bit of that code.

Bronze challenges usually involve either doing something very similar to what you’ve already done in the chapter, or making a simple change to it. So for those I usually wind up just referring back to the book. Gold challenges usually involve doing something completely new, not talked about in the chapter, so referring back to the book generally isn’t much help & you need to look elsewhere. Silver challenges are somewhere in the middle. However, that’s not always the case - this one was a bit more challenging than the usual Bronze challenge.

I mostly refer to the Apple documentation online if I need outside help. A couple times when I find myself seriously floundering, I’ll just type a few relevant questions into Google; usually somebody’s asked that question somewhere before. Frequently those searches have lead to Stack Overflow, but it’s not so overwhelming when you get targeted to a specific discussion that’s relevant to what you’re doing. And even if the discussion didn’t tell me exactly what I wanted to know, it usually gave me an idea or two for what to look at next.

Since the challenge here is to change how the table view presents the data, and the UITableViewProtocol is how the table view finds out about the data it’s presenting, the documentation for that protocol is where you should start looking. There are a few additional functions in the protocol that you need to implement. In addition to the one you found that returns the number of sections, this function is where the table view asks for the section titles:

override func tableView(_ tableView: UITableView,
                        titleForHeaderInSection section: Int) -> String?

And this function is where it asks how many rows (ie, items) are in each section:

override func tableView(_ tableView: UITableView,
                        numberOfRowsInSection section: Int) -> Int

Another thing to keep in mind is that it’s OK to iterate over your solution a few times. If at first you don’t see a better solution than to hard code the number of sections, go ahead & do it that way. If you think of a better solution later (and IMO there are better solutions), you know exactly where to make the change. For instance, once I had my solution working for two sections, I asked myself how hard would it be to add a third section? A fourth section? Those questions lead me to additional changes, and I was ultimately able to add or remove sections by adding or deleting a single line of code. But I didn’t get there all at once.

This post was flagged by the community and is temporarily hidden.

You just contradicted yourself when you posted a link to a website that is trying to sell a course which claims to teach how to write code.

Answering your question on how you could create and name each section:

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 2
    }
    
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if section == 0 {
            return "Under 50 USD"
        } else {
            return "Over 50 USD"
        }
    }

Thanks,

A better question I want to ask you at this point is: what was your thought process that led you to override these functions the way you did?

So here’s what I’ve tried so far…

var sections: [String] = ["Greater than $50", "$50 or less"]

// Figure out where that item is in the array
    if let index = itemStore.allItems.firstIndex(of: newItem) {
        
        let indexPath = IndexPath(row: index, section: placeInSection(item: newItem))
        
        // insert this new row into the table
        tableView.insertRows(at: [indexPath], with: .automatic)
        
        print("Inserted item in section \(indexPath.section), row \(indexPath.row)")
        
        
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return itemStore.allItems.count
} // didn't change this from the chapter

 // Bronze Challenge - this is where I try to assign an item's section
func placeInSection (item: Item) -> Int {
    if item.valueInDollars > 50 {
        return 0
    } else {
        return 1
    }
}

// Create a standard header that includes the returned text.
override func tableView(_ tableView: UITableView, titleForHeaderInSection
                            section: Int) -> String? {
    if section == 0 {
        return sections[0]
    } else {
        return sections[1]
    }
}

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

When I run the app, I can get my sections:

But once I hit Add, the app crashes and the log begins with these messages:

2020-06-19 16:06:00.217373-0400 LootLogger[10179:1908055] *** Assertion failure in -[UITableView _Bug_Detected_In_Client_Of_UITableView_Invalid_Number_Of_Rows_In_Section:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3899.22.15/UITableView.m:2401
2020-06-19 16:06:00.224030-0400 LootLogger[10179:1908055] *** 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 (0), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'

I thought about adding an integer indicator for section on Item.Swift, but that didn’t make a difference.

Why can’t I assign a section using my placeInSection function?

You’ve got a few problems here. First, you didn’t update this function:

This function is still acting like all items are being displayed in a single section, but that isn’t true anymore. If section 0 is the section for items greater than $50, then when this function is called with numberOfRowsInSection set to 0 you need to return the number of items greater than $50, not the total number of all items.

Second, you need to update

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

The original implementation of this function ignored the section number inside indexPath because everything was being displayed in a single section. But now you have two sections, so you need to start taking that into account, so you can return an item for the proper section.

Exactly how you change these two functions is going to depend on how you are handling the section split in your ItemStore. Either you have two ItemStore arrays, one for < 50 and one for > 50, and the section number tells you which one to access, or you still have a single ItemStore and you use filtering to get the <50 or >50 items, in which case the section number tells you which filter to apply

I think that the key problem might be in your cellForRowAt method which I can’t look into. So as far as I understand, and don’t take my word for it because I’m just a beginner like you, what happens when you tell the tableView to insertRowAt is that it doesn’t really insert the row it just runs some methods like cellForRowAt again exactly for that row.

CellForRowAt will then show the row for the indexPath you provided in the insertRowAt. So if you don’t have it set it up here inside cellForRowAt that this item which has a value greater than 50 USD should go to section 0 and the ones with a value less/equals than 50 USD should go to section 1, it will not show the row as you expected which then creates a conflict.

In my cellForRowAt I check which section the method is called on, something like this:
if indexPath.section == 0 {
// go grab the item for this row with help from indexPath.row
}
same for indexPath.section == 1
The complexity here lies in answering the question “how do you find that item?”.
How do you find the item that should go on row 0 and section 1 for example which would be the first item over 50 USD if in your allItems array they are all mixed up (values over and under 50 USD all together), you can’t rely on the row no more.
I solved this by creating 2 arrays, one for values over 50 USD and another for values under/equals 50 USD.
Inside addNewItem:

  1. I create an item randomly (nothing new here)
  2. If its value is over 50 USD append to a certain array, if it isn’t append to the other.
  3. call insertRowAt, just like you did. Same logic here.

Inside cellForRowAt(which I think is what you are missing)

  1. Find out on what section the cell will be placed in (if indexPath.section …)
  2. Depending on the section I grab the item from the appropriate array, since in this array all the values are either over 50 USD or under/equals, I can just use indexPath.row to get the item for that row.
  3. return cell.

Well, I knew that I needed 2 sections. So I returned that in numberOfSections.
And I also knew how each section should be called.
I think what you really need to know is, this methods run before the tableView appears to us.
TableView needs to know how many sections it needs to show, so I tell it.
Then it wants to know how they will be called, which I don’t think is mandatory to set.
So when it is asking me “What is the title you want for section 0”, I say “Under 50 USD”.
And when it is asking the same question for section 1, I say “Over 50 USD”.
It won’t ask me anything about section 2, because I said there will be only 2 sections.

This is how I understand it, if this is how it really happens under the hood is still unknown to me.

Tried these suggestions (I think?) But the app still crashes:

class ItemsViewController : UITableViewController{

var itemStore: ItemStore!

var sections: [String] = ["Greater than $50", "$50 or less"]

// Create two arrays of Items

var overFifties = [Item]()
var underFifties = [Item]()

    @IBAction func addNewItem(_ sender: UIButton) {
// Create a new item and add it to the store
    let newItem = itemStore.createItem()
    
    
    
    if newItem.valueInDollars > 50 {
        overFifties.append(newItem)
    } else {
        underFifties.append(newItem)
    }

// Figure out where that item is in the array
    if let index = itemStore.allItems.firstIndex(of: newItem) {
        
        if overFifties.firstIndex(of: newItem) != nil {
            let indexPath = IndexPath(row: index, section: 0)
            // insert this new row into the table
            tableView.insertRows(at: [indexPath], with: .automatic)
            print("Inserted item in section \(indexPath.section), row \(indexPath.row)")
        } else {
            let indexPath = IndexPath(row: index, section: 1)
            // insert this new row into the table
            tableView.insertRows(at: [indexPath], with: .automatic)
            print("Inserted item in section \(indexPath.section), row \(indexPath.row)")
            
        }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    
    var topItems = [Item]()
    var lowItems = [Item]()
    
    for item in itemStore.allItems {
        if item.valueInDollars > 50 {
            topItems.append(item)
        } else {
            lowItems.append(item)
        }
    }
    
    if section == 0 {
        return topItems.count
    } else {
        return lowItems.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
    
    if indexPath.section == 0 {
        let item = overFifties[indexPath.row]
        cell.textLabel?.text = item.name
        cell.detailTextLabel?.text = "$\(item.valueInDollars)"
        
        return cell
    } else {
        let item = underFifties[indexPath.row]
        cell.textLabel?.text = item.name
        cell.detailTextLabel?.text = "$\(item.valueInDollars)"
        
        return cell
    }

    // Create a standard header that includes the returned text.
override func tableView(_ tableView: UITableView, titleForHeaderInSection
                            section: Int) -> String? {
    if section == 0 {
        return sections[0]
    } else {
        return sections[1]
    }
}

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

}

I can add new items under a single section…

But once an Item’s valueInDollars jumps to the other side, the app crashes:

LootLogger[16468:2053591] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3899.22.15/UITableView.m:2155
2020-06-19 21:20:16.283119-0400 LootLogger[16468:2053591] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert row 4 into section 0, but there are only 1 rows in section 0 after the update'

__

  1. The repeating code is fugly. I can’t believe the authors had this in mind as part of this challenge’s solution.
  2. I think it has something to do with using allItems, in that the app is trying to add row number x to an empty section. But I can’t find a way to reset the row value.

Seriously, this shouldn’t take days to complete.

You shouldn’t be creating new Item arrays in the ItemsViewController, your items are stored in ItemStore. You need to update ItemStore to organize the data the way you want it, not try to create some new data storage that sits beside it.

As for the error you get when you crash, in addNewItem, you have two lines of code that set the index path for the new row in the table view, but they are using an index value from itemStore.allItems. That won’t work because there is no longer a 1 to 1 equivalence between the index number for an item in itemStore and the row number on the display. There are now two sets of row numbers, one set for each section. In this particular crash, you added a new item as the fifth item in the itemStore, so it has an index of 4, but it’s the first item in the over 50 section so it’s row number should be 0.

I think the easiest approach for you is to modify your ItemStore so it has two arrays, one for each section. Or a two dimensional array with the section number as the first index. Get rid of the extra arrays you’ve added to ItemViewController, you don’t need any of those. Once you do this, it will fix the row number/index number confusion - the row numbers for each section will map to the index values for each array.

I’m a little closer, but my items that cost $50 or less do not appear on my screen correctly, and it crashes…

First, my changes:

ItemStore.swift:

var over50Items = [Item]()
var under50Items = [Item]()

@discardableResult func createItem() -> Item {
    let newItem = Item(random: true)
    
    
    if newItem.valueInDollars > 50 {
        over50Items.append(newItem)            
    } else {
        under50Items.append(newItem)
    }
    
    
    return newItem
}

func removeItem(_ item: Item) {
    if let index = over50Items.firstIndex(of: item) {
        over50Items.remove(at: index)
    } else if let index = under50Items.firstIndex(of: item){
        under50Items.remove(at: index)
    }
}

// Listing 9.21 p 209 - Reordering items within the store
func moveItem(from fromIndex: Int, to toIndex: Int) {
    if fromIndex == toIndex {
        return
    }
    
    // Get reference to object being moved so you can reinsert it
    
    let movedOver50Item = over50Items[fromIndex]
    
    // Remove item from array
    over50Items.remove(at: fromIndex)
    
    // insert item in array at new location
    over50Items.insert(movedOver50Item, at: toIndex)
    
    let movedUnder50Items = under50Items[fromIndex]
    under50Items.remove(at: fromIndex)
    under50Items.insert(movedUnder50Items, at: toIndex)
}

}

Changes to ItemViewController.swift

class ItemsViewController : UITableViewController{

var itemStore: ItemStore!
let sections = ["Over $50","$50 or Less"]

@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()
    
    // Figure out where that item is in the array
    if let index = itemStore.over50Items.firstIndex(of: newItem) {
        
        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)")
        
        
        
    } else if let index = itemStore.under50Items.firstIndex(of: newItem) {
        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, numberOfRowsInSection section: Int) -> Int {

    if section == 0 {
        return itemStore.over50Items.count
    } 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)
    
    // 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 itemStore.over50Items.firstIndex(of: itemStore.over50Items[indexPath.row]) != nil {
        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 {
        
        if itemStore.over50Items.firstIndex(of: itemStore.over50Items[indexPath.row]) != nil {
            // 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 {
            itemStore.removeItem(itemStore.under50Items[indexPath.row])
            // Also remove that row from the table view with an animation
            tableView.deleteRows(at: [indexPath], with: .automatic)
            
        }
        
        
    }
}

// listing 9.22 p 209 Implementing table view row reodering
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    // update the model
    itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row)
}
 // Create a standard header that includes the returned text.
override func tableView(_ tableView: UITableView, titleForHeaderInSection
                            section: Int) -> String? {
    if section == 0 {
        return sections[0]
    } else {
        return sections[1]
    }
}

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

Output:

Console:

Item Name: Fluffy Bear
Value: 89
Item Name: Rusty Bear
Value: $25
Fatal error: Index out of range
2020-06-20 14:43:57.162940-0400 LootLogger[27239:2242833] Fatal error: Index out of range
(lldb) 

Notice that the console displays the correct second item, while the first item duplicates
on screen in the second section.

In tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath), you need to check indexPath.section to determine whether to look in over50Items or under50Items.

Also, your moveItem function is trying to move the same item in both over50Items and under50Items. It should never have to do that - the user can only move one item at a time and that item can’t be in both arrays.

I can display the correct items under their appropriate sections.
But I find that I can move items between sections. And the app crashes if I move an item from the longer list to the shorter one.

ItemsViewController.swift:

    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
    
    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
    }
    
}
...
    override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    // update the model
    
    itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row, within: sourceIndexPath.section)
   
}

ItemStore.swift:

    func moveItem(from fromIndex: Int, to toIndex: Int, within section: Int) {
    if fromIndex == toIndex {
        return
    }
    
    // Get reference to object being moved so you can reinsert it
    // which ItemList is this in???
    // Do I identify the correct ItemList by adding section as an argument to the function?
    
    if section == 0 {
        // do this within over50Items
        let movedItem = over50Items[fromIndex]
        
        // remove item from array
        over50Items.remove(at: fromIndex)
        
        // insert item in array at new location
        over50Items.insert(movedItem, at: toIndex)
    } else {
        // do this within under50Items
        let movedItem = under50Items[fromIndex]
        
        // remove item from array
        under50Items.remove(at: fromIndex)
        
        // insert item in array at new location
        under50Items.insert(movedItem, at: toIndex)
    }

moving things around on my screen…

Console log:

Item Name: Fluffy Mac
Value: $63
Item Name: Rusty Mac
Value: $62
Item Name: Fluffy Mac
Value: $93
Item Name: Rusty Spork
Value: $5
Item Name: Fluffy Mac
Value: $9
...
Fatal error: Array index is out of range
2020-06-21 16:52:01.517439-0400 LootLogger[58513:2730900] Fatal error: Array     index is out of range

(lldb)

Why won’t these updates restrict section? I don’t see where they’re spontaneously updating section designation as I reorder items.

The table view can tell if you’re dragging things from one section to the other. If you compare the section numbers passed into tableView(:moveRowAt:to:), you can see that’s happening. But the table view doesn’t understand what the sections represent to the user, or whether such moves should be allowed or not. If you want to stop moves between sections, this should help.

Finally got it to work by adding that function:

// Bronze challenge
override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
    
    // I need to identify section
    let sourceSection = sourceIndexPath.section
    let destinationSection = proposedDestinationIndexPath.section
    
    // only allow the move if sections are the same, otherwise, slide them within the same section
    if destinationSection < sourceSection {
        return IndexPath(row: 0, section: sourceSection)
    } else if destinationSection > sourceSection {
        return IndexPath(row: self.tableView(tableView, numberOfRowsInSection: sourceSection)-1, section: sourceSection)
    }
    
    return proposedDestinationIndexPath
    
}

Thanks for your help. This took me days to complete. Realistically, how long should it have taken?