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

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?

Challenges in this chapter are taking me days to complete, too, so I think that’s pretty normal/expected. Keep up!

My implementation (not perfect but works):

ItemStore.swift

class ItemStore {

    var moreThan50 = [Item]()
    var rest = [Item]()
    var sections: [[Item]]
    var sectionNames = ["Less than or equal to 50", "More than 50"]
    
    init() {
        sections = [rest, moreThan50]
    }
    
    @discardableResult func createItem() -> Item {
        let newItem = Item(random: true)

        if newItem.valueInDollars > 50 {
            sections[1].append(newItem)
        } else {
            sections[0].append(newItem)
        }

        return newItem
    }
    
    func removeItem(at indexPath: IndexPath) {
        sections[indexPath.section].remove(at: indexPath.row)
    }
    
    func moveItem(from fromIndexPath: IndexPath, to toIndexPath: IndexPath) {
        if fromIndexPath == toIndexPath {
            return
        }
        
        assert(fromIndexPath.section == toIndexPath.section)

        let movedItem = sections[fromIndexPath.section][fromIndexPath.row]

        sections[fromIndexPath.section].remove(at: fromIndexPath.row)

        sections[toIndexPath.section].insert(movedItem, at: toIndexPath.row)
    }

}

ItemsViewController.swift

class ItemsViewController: UITableViewController {

    var itemStore: ItemStore!
    
    @IBAction func addNewItem(_ sender: UIButton) {
        let newItem = itemStore.createItem()
        
        for sectionIndex in 0..<itemStore.sections.count {
            if let index = itemStore.sections[sectionIndex].firstIndex(of: newItem) {
                let indexPath = IndexPath(row: index, section: sectionIndex)
                tableView.insertRows(at: [indexPath], with: .automatic)
                break
            }
        }
    }

    @IBAction func toggleEditingMode(_ sender: UIButton) {
        if isEditing {
            sender.setTitle("Edit", for: .normal)

            setEditing(false, animated: true)
        } else {
            sender.setTitle("Done", for: .normal)

            setEditing(true, animated: true)
        }
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return itemStore.sections.count
    }
    
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return itemStore.sectionNames[section]
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return itemStore.sections[section].count
    }
    
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {

            itemStore.removeItem(at: indexPath)

            tableView.deleteRows(at: [indexPath], with: .automatic)
        }
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)

        let item = itemStore.sections[indexPath.section][indexPath.row]

        cell.textLabel?.text = item.name
        cell.detailTextLabel?.text = "$\(item.valueInDollars)"

        return cell
    }
    
    override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
        if sourceIndexPath.section == proposedDestinationIndexPath.section {
            return proposedDestinationIndexPath
        } else {
            return sourceIndexPath
        }
    }
    
    override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        itemStore.moveItem(from: sourceIndexPath, to: destinationIndexPath)
    }

}
1 Like

Because this thread helped me, I thought I’d add my own contribution.

My opinion and experience with this is that it’s a lot easier with a multidimensional array. I think there’s less redundancy in the code vs. using two arrays, and the changes needed to make it work in ItemStore and ItemsViewController are pretty minor. Mostly I needed to add the section number to the allItems array calls (that is, allItems[sectionNumber] etc. depending on what I was doing).

Here’s what I did.

Item.swift -> added var valueGT50 = false. In init, if valueInDollars > 50, self.valueGT50 =. true. I also added this comparison to the function for Equatable.

ItemStore.swift -> changed allItems to (those are empty square brackets. Creating empty elements in the array was necessary for append and other functions in ItemsViewController to work). createItem was also modified.

class ItemStore
{
var allItems : [[Item]] = [,]

@discardableResult func createItem() -> Item
{
    let newItem = Item(random: true)
    var sectionNum = 0
    
    if newItem.valueGT50
    {
        sectionNum = 1
    }
    
    allItems[sectionNum].append(newItem)
    
    return newItem
}

func removeItem(_ item: Item)
{
    var sectionNum = 0
    
    if item.valueGT50
    {
        sectionNum = 1
    }

    if let index = allItems[sectionNum].firstIndex(of: item)
    {
        allItems[sectionNum].remove(at: index)
    }
}

func moveItem(from fromIndex: IndexPath, to toIndex: IndexPath)
{
    if fromIndex == toIndex
    {
        return
    }
    
    // get reference to object being moved so it can be reinserted
    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[fromIndex.section].insert(movedItem, at: toIndex.row)
}

}

ItemsViewController.swift looks like this. There are a couple of added functions which are also described in the thread above and some of the existing functions were modified to include the section number in the array call.

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()
    var sectionNum = 0
    
    if newItem.valueGT50
    {
        sectionNum = 1
    }

    // figure out where that item is in the array
    if let index = itemStore.allItems[sectionNum].firstIndex(of: newItem)
    {
        let indexPath = IndexPath(row: index, section: sectionNum)
        
        // 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
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return itemStore.allItems[section].count
}

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

    // get a new or reused 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 the table view
    let item = itemStore.allItems[indexPath.section][indexPath.row]
    
    cell.textLabel?.text = item.name
    cell.detailTextLabel?.text = "$\(item.valueInDollars)"
    
    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]
        
        // remove the item
        itemStore.removeItem(item)
        
        // 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) {
    // update the model
    itemStore.moveItem(from: sourceIndexPath, to: destinationIndexPath) // remove .row references to send entire indexPath for each item
}

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    if section == 0
    {
        return "$50 or less"
    }
    else
    {
        return "Over $50"
    }
}


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
    
}

}

I probably don’t need valueGT50 as part of the Item, but it was left over from my first pass at the challenge, where I was trying to do this with a one dimensional array. I gave up on that path when I realized that indexPath wasn’t giving me enough information to find the element I needed. In fact, my first working pass at two sections would correctly put the first elements in a section, but as soon as the other section had elements going to it, it was showing the elements from the first section. Fun bug! :smile:

Again, thanks for the help. I hope someone else might find this useful.

1 Like

BTW, I’m not sure what I’m doing wrong with the pasted code in my previous post. It’s surrounded by < code > </ code > tags yet not all of the code in between the tags is scrolling together.

Paste your code between a pair of three back-tick (```) characters like this:
```
Code…
```

For Example:

auto bar () -> long;

auto foo () -> long {
   return bar ()
}
1 Like

ItemsViewController

import UIKit

class ItemsViewController: UITableViewController {
    
    var itemStore: ItemStore!
    let sectionsArray = ["Above $50", "Below $50"]
    
    @IBAction func addNewItem(_ sender: UIButton) {
        let newItem = itemStore.createItem()
        
        if newItem.valueInDollars > 50 {
        if let index = itemStore.aboveFifty.firstIndex(of: newItem) {
            let indexPath = IndexPath(row: index, section: 0)
            
            tableView.insertRows(at: [indexPath], with: .automatic)
        }
        } else {
            if let index = itemStore.belowFifty.firstIndex(of: newItem) {
                let indexPath = IndexPath(row: index, section: 1)
                
                tableView.insertRows(at: [indexPath], with: .automatic)
            }
        }
    }
    
    @IBAction func toggleEditingMode(_ sender: UIButton) {
        if isEditing {
            sender.setTitle("Edit", for: .normal)
            
            setEditing(false, animated: true)
        } else {
            sender.setTitle("Done", for: .normal)
            
            setEditing(true, animated: true)
        }
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return sectionsArray.count
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if section == 0 {
            return itemStore.aboveFifty.count
        } else {
            return itemStore.belowFifty.count
        }
    }
    
    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if section == 0 {
            return sectionsArray[0]
        } else {
            return sectionsArray[1]
        }
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//        let cell = UITableViewCell(style: .value1, reuseIdentifier: "UITableViewCell")
        let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
        
        if indexPath.section == 0 {
            let item = itemStore.aboveFifty[indexPath.row]
            cell.textLabel?.text = item.name
            cell.detailTextLabel?.text = "$\(item.valueInDollars)"
        } else {
            let item = itemStore.belowFifty[indexPath.row]
            cell.textLabel?.text = item.name
            cell.detailTextLabel?.text = "$\(item.valueInDollars)"
        }
        return cell
    }
    
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            
            if indexPath.section == 0 {
            let item = itemStore.aboveFifty[indexPath.row]
            itemStore.removeItem(item)
            tableView.deleteRows(at: [indexPath], with: .automatic)
            } else {
                let item = itemStore.belowFifty[indexPath.row]
                itemStore.removeItem(item)
                tableView.deleteRows(at: [indexPath], with: .automatic)
            }
        }
    }

    override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row)
    }
}

Item

import UIKit

class Item: Equatable {
    
    static func ==(lhs: Item, rhs: Item) -> Bool {
        return lhs.name == rhs.name
        && lhs.serialNumber == rhs.serialNumber
        && lhs.valueInDollars == rhs.valueInDollars
        && lhs.dateCreated == rhs.dateCreated
    }
    var name: String
    var valueInDollars: Int
    var serialNumber: String?
    let dateCreated: Date
    
    init(name: String, serialNumber: String?, valueInDollars: Int) {
        self.name = name
        self.serialNumber = serialNumber
        self.dateCreated = Date()
        self.valueInDollars = valueInDollars
    }
    
    convenience init(random: Bool = false) {
        if random {
            let adjectives = ["Fluffy", "Rusty", "Shiny"]
            let nouns = ["Bear", "Spork", "Mac"]
            
            let randomAdjective = adjectives.randomElement()!
            let randomNoun = nouns.randomElement()!
            
            let randomName = "\(randomAdjective) \(randomNoun)"
            let randomValue = Int.random(in: 0..<100)
            let randomSerialNumber = UUID().uuidString.components(separatedBy: "-").first!
            
            self.init(name: randomName, serialNumber: randomSerialNumber, valueInDollars: randomValue)
        } else {
            self.init(name: "", serialNumber: nil, valueInDollars: 0)
        }
    }
}

ItemStore

import UIKit

class ItemStore {
    
//    var allItems = [Item]()
    var belowFifty = [Item]()
    var aboveFifty = [Item]()
    
    @discardableResult func createItem() -> Item {
        let newItem = Item(random: true)
        if newItem.valueInDollars > 50 {
            aboveFifty.append(newItem)
        } else {
            belowFifty.append(newItem)
        }
        return newItem
    }
    
    func removeItem(_ blank: Item) {
        if blank.valueInDollars > 50 {
        if let index = aboveFifty.firstIndex(of: blank) {
            aboveFifty.remove(at: index)
            }
        } else {
            if let index = belowFifty.firstIndex(of: blank) {
                belowFifty.remove(at: index)
                }
        }
    }

    func moveItem(from fromIndex: Int, to toIndex: Int) {
        if fromIndex == toIndex {
            return
        }

        let sectionZero = aboveFifty[fromIndex]
        aboveFifty.remove(at: fromIndex)
        aboveFifty.insert(sectionZero, at: toIndex)
        
        let sectionOne = belowFifty[fromIndex]
        belowFifty.remove(at: fromIndex)
        belowFifty.insert(sectionOne, at: toIndex)
    }
}

I’m extremely new to coding and had a very hard time even reading through the chapters. The above is what I came up with, mostly cutting/pasting already used code… I was only able to modify the already written code, I couldn’t think of any better solutions by myself, I feel terrible because of that… Is this normal? Should I try harder?