Chapter 11: Bronze, Silver, & Gold Challenge - A Different Approach


#1

I may be crazy, but I approached this from a different perspective. That is, rather than having the controller assume any knowledge about the itemStore, I had it query the itemStore about every aspect of its display. My goal was to be able to drop in any kind of “itemStore” and have the app and view controller be able to work with it (kind of an extension of the “dependency inversion principle” from the beginning of the chapter). I didn’t set up my own “ItemStoreDelegate”, but I think the results are similar and would lend themselves to the creation of a delegate if this app was actually one that might eventually be extended for other purposes.

Anyway, here’s my code for anyone who is interested:

Here is my itemStore.swift code:

import UIKit

class ItemStore {
let sections = 2
var allItems = [Item, Item]

init() {
    let noMoreItems = Item(name: "No more items!", serialNumber: nil, valueInDollars: 0)
    allItems[1].append(noMoreItems)
}
func populate(cell: UITableViewCell, for indexPath: IndexPath) -> Void {
    // 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 tableview
    let item = allItems[indexPath.section][indexPath.row]

    cell.textLabel?.text = item.name
    if indexPath.section == sections - 1 {
        cell.detailTextLabel?.text = nil
        cell.textLabel?.textColor = UIColor.lightGray
    }  else   {
        cell.detailTextLabel?.text = "$\(allItems[indexPath.section][indexPath.row].valueInDollars)"
        cell.textLabel?.textColor = UIColor.black
    }
}

func indentationLevel(forIndexPath indexPath: IndexPath) -> Int {
    if indexPath.section == sections - 1  {
        return 2
    }  else  {
        return 0
    }
}
func editingStyle(forIndexPath indexPath: IndexPath) -> UITableViewCellEditingStyle  {
    if indexPath.section == sections - 1 {
       return  UITableViewCellEditingStyle.none
    }  else  {
        return UITableViewCellEditingStyle.delete
    }
}
func canMove(rowAt indexPath: IndexPath) -> Bool {
    if indexPath.section == sections - 1 {
        return  false
    }  else  {
        return true
    }
}

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

func removeItem(_ indexPath: IndexPath) {
    allItems[indexPath.section].remove(at: indexPath.row)
}

func moveItem(from fromIndex: IndexPath, to toIndex: IndexPath) -> Bool {
    if fromIndex == toIndex  ||  toIndex.section == sections - 1  {
        return false
    }
    
    // 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)
    
    return true
}

}

And my ItemsViewController.swift code:

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()
    
    // Figure out where that item is in the array
    if let index = itemStore.allItems[0].index(of: newItem)
    {
        let indexPath = IndexPath(row: index, section: 0)
        // Insert this new row into the table
        tableView.insertRows(at: [indexPath], with: .automatic)
    }
}

@IBAction func toggleEditingMode(_ sender: UIButton)
{
    // If ou 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 viewDidLoad() {
    super.viewDidLoad()
    
    // Get the height of the status bar
    let statusBarHeight = UIApplication.shared.statusBarFrame.height
    
    let insets = UIEdgeInsets(top: statusBarHeight, left: 0, bottom: 0, right: 0)
    tableView.contentInset = insets
    tableView.scrollIndicatorInsets = insets
}

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

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 recycled cell
    let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
    // Have the item store populate the cell
    itemStore.populate(cell: cell, for: indexPath)
    
    return cell
}
override func tableView(_ tableView: UITableView, indentationLevelForRowAt: IndexPath) -> Int
{
    return itemStore.indentationLevel(forIndexPath: indentationLevelForRowAt)
}
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle
{
    return itemStore.editingStyle(forIndexPath: indexPath)
}
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool
{
    return itemStore.canMove(rowAt: indexPath)
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, 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]
        
        let title = "Delete \(item.name)?"
        let message = "Are you sure you want to delete this item?"
        let ac = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
        
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        ac.addAction(cancelAction)
        
        let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler:
            {   (action) -> Void in
        
                // Remove the item from the store
                self.itemStore.removeItem(indexPath)
        
                // Also remove that row from the table view with an animation
                self.tableView.deleteRows(at: [indexPath], with: .automatic)
            })
        ac.addAction(deleteAction)
        
        // Present the alert controller
        present(ac, animated: true, completion: nil)
    }
}

override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
{
    // Update the model for the move
    if !itemStore.moveItem(from: sourceIndexPath, to: destinationIndexPath)
    {
        // If the move didn't happen in the item store, reload the tableView to match the data
        tableView.reloadSections(IndexSet(integersIn: 0..<itemStore.sections), with: .automatic)
    }
}

override func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String?
{
    return "Remove"
}

}


#2

Hello, everyone!
I think there is a better approach for solving Gold Challenge.
There is a method in UITableViewDelegate named tableView(_:targetIndexPathForMoveFromRowAt:toProposedIndexPath:).

My solution is as follows (in case that last cell is not allowed to move):

    override func tableView(_ tableView: UITableView,
                        targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath,
                        toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
    if proposedDestinationIndexPath.row == itemStore.allItems.endIndex - 1 {
        return sourceIndexPath
    }
    return proposedDestinationIndexPath
}

And here is my ItemStore class:

class ItemStore {
var allItems = Item

@discardableResult func createItem() -> Item {
    let newItem = Item(random: true)
    allItems.insert(newItem, at: allItems.endIndex - 1)
    return newItem
}

func removeItem(_ item: Item) {
    if let index = allItems.index(of: item) {
        allItems.remove(at: index)
    }
}

func moveItem(from fromIndex: Int, to toIndex: Int) {
    if fromIndex == toIndex {
        return
    }
    if (fromIndex > 0 && toIndex > 0), (fromIndex < allItems.count && toIndex < allItems.count) {
        let movedItem = allItems[fromIndex]
        allItems.remove(at: fromIndex)
        allItems.insert(movedItem, at: toIndex)
    }
}

init() {
    let placeHolderItem = Item(name: "No more items", serialNumber: nil, valueInDollars: 0)
    allItems.append(placeHolderItem)
}

}


#3

Here’s my solution for making the ‘No more items!’ row undeletable in the Gold Challenge. I found this UITableViewDelegate method as I read the documentation:

override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
    if indexPath.row == itemStore.allItems.count {
        return UITableViewCellEditingStyle.none
    }
    
    return UITableViewCellEditingStyle.delete
}

#4

My solution to silver and gold challenges. First create an itemstore with one newItem set to random false and give it an accessibility hint of “emptyItem”. This will have “NO MORE ITEMS” as text later. Insert all other items at 0.

   @discardableResult func createItem () -> Item {
            var newItem = Item ()
            
            if allItems.count == 0 {
                newItem = Item(random: false)
                newItem.accessibilityHint = "emptyItem"
            } else {
                newItem = Item(random: true)
                newItem.accessibilityHint = newItem.name
            }
            
            allItems.insert(newItem, at: 0)
            
            return newItem
        }
        
        init() {
            for _ in 0..<1 {
                createItem()
            }
        }

Now make changes in the ItemViewController. Using if statements and the accessibility hints one can differentiate the cells. Make changes to the content of the last row here.

 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 tableView
        let item = itemStore.allItems[indexPath.row]
        
        if item.name != "" {
            cell.textLabel?.font = UIFont.systemFont(ofSize: 20.0)
            cell.textLabel?.text = item.name
            cell.detailTextLabel?.text = "$\(item.valueInDollars)"
        }
        
        if item.accessibilityHint == "emptyItem" {
            cell.detailTextLabel?.text = ""
            cell.textLabel?.font = UIFont.boldSystemFont(ofSize: 16.0)
            cell.textLabel?.text = "           NO MORE ITEMS"
        }
        
        return cell
    }

Remove the delete function from the last row.

 override func tableView(_ tableView: UITableView,
                            editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
        let tableCell = itemStore.allItems[indexPath.row]
        
        // Selecting the last row with accessibility hint "emptyItem"
        if tableCell.accessibilityHint == "emptyItem" {
            return UITableViewCellEditingStyle.none
        } else {
            return UITableViewCellEditingStyle.delete
        }
    }

Exclude the last row from being moved.

override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        let rowData = itemStore.allItems[indexPath.row]
        
        // Allow moving only to rows with an item name
        return rowData.accessibilityHint != "emptyItem"
    }

Do not allow moving rows beyond the last row.

override func tableView(_ tableView: UITableView,
                            moveRowAt sourceIndexPath: IndexPath,
                            to destinationIndexPath: IndexPath) {
        let rowData = itemStore.allItems
        // Update the Model
        if destinationIndexPath.row < rowData.count-1 {
            itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row)
        }
        self.tableView.reloadData ()
    }

#5

I approached the Silver Challenge from a totally different ( perhaps incorrect?) angle.

I did not use any code, but instead took the hint from Figure 11.2 (page 196) , when we added a table view header and the two Buttons to the header area. For the challenge, I simply added a new view to the Table View Footer area, and then followed up with a new label with the text “No more items!”. Voila! This label is always the last row and is of course undeletable.