Solution for Ch 10 Bronze, Silver, and Gold Challenge

In class ItemStore, define two computed properties highValueItems and otherItems. The array transformation instance method filter(_:slight_smile: is used to filter items with valueInDollars greater than 50.

ItemStore.swift

class ItemStore {
  var allItems = [Item]()
  
  var highValueItems: [Item] {
    return allItems.filter{ $0.valueInDollars > 50 }
  }
  
  var otherItems: [Item] {
    return allItems.filter{ $0.valueInDollars <= 50 }
  }

ItemsViewController.swift

class ItemsViewController: UITableViewController {
  var itemStore: ItemStore!

  override func numberOfSections(in tableView: UITableView) -> Int {
    return 2
  }
  
  override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    switch section {
    case 0:
      return "More than $50"
    default:
      return "Others"
    }
  }
  
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    switch section {
    case 0:
      return itemStore.highValueItems.count
    default:
      return itemStore.otherItems.count + 1   // 1 for "No more items!"
    }
  }
  
  override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    if indexPath.section == 1 && indexPath.row == itemStore.otherItems.count {
      return 44
    } else {
      return 60
    }
  }
  
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
    
    let items: [Item]
    switch indexPath.section {
    case 0:
      items = itemStore.highValueItems
    default:
      items = itemStore.otherItems
    }
    
    if indexPath.section == 1 && indexPath.row == items.count {
      cell.textLabel?.text = "No more items!"
      cell.detailTextLabel?.text = ""
    } else {
      let item = items[indexPath.row]
      cell.textLabel?.text = item.name
      cell.detailTextLabel?.text = "\(item.valueInDollars)"
      cell.textLabel?.font = UIFont.systemFont(ofSize: 20)
      cell.detailTextLabel?.font = UIFont.systemFont(ofSize: 20)
    }
    
    return cell
  }
2 Likes

Another implementation by creating a separate section just for the “No more items!”

override func numberOfSections(in tableView: UITableView) -> Int {
  return 3
}

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  switch section {
  case 0:
    return "More than $50"
  case 1:
    return "Others"
  default:
    return nil    // For "No more items!"
  }
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  switch section {
  case 0:
    return itemStore.highValueItems.count
  case 1:
    return itemStore.otherItems.count
  default:
    return 1   // For "No more items!"
  }
}

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  if indexPath.section <= 1 {
    return 60
  } else {
    return 44   // For "No more items!"
  }
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
  if indexPath.section <= 1 {
    let item = indexPath.section == 0 ? itemStore.highValueItems[indexPath.row]: itemStore.otherItems[indexPath.row]
    cell.textLabel?.text = item.name
    cell.detailTextLabel?.text = "\(item.valueInDollars)"
    cell.textLabel?.font = UIFont.systemFont(ofSize: 20)
    cell.detailTextLabel?.font = UIFont.systemFont(ofSize: 20)
  } else {
    cell.textLabel?.text = "No more items!"
    cell.detailTextLabel?.text = ""
  }
  
  return cell
}
2 Likes

To setup the TableView background picture:

A picture named background.png is added to the Assets.xcassets
In ItemsViewController.swift, add the code below at the end of viewDidLoad()

let imageView = UIImageView(image: UIImage(named: "background"))
imageView.contentMode = .scaleAspectFit
tableView.backgroundView = imageView

To make the background look better, in tableView(_:cellForRowAt:) add the code below before return cell

cell.backgroundColor = UIColor.clear

Screen shot of TableView with background picture

1 Like

Below is my solution for the Silver Challenge. There are a few extra lines included for extra modifications necessary as I proceeded to chapter 13 (which I’ve just completed.) I came to point out that I thought it’d be a good idea for the next edition of this book to include challenges at the end of each chapter that build on previous challenges.

As a result of my particular implementation (which I’ve left in place for subsequent chapters), in each chapter that followed I’ve had to make modifications to ensure the app behaves as one might expect, and also doesn’t crash. This has required implementing various UITableViewDelegate methods and making other changes. It’s taught me a lot more about the API and iOS programming in general.

For example, in chapter 13 I had to make sure the “No more items!” cell could not be selected so it wouldn’t segue to the new view and try to display an item that doesn’t exist. I accomplished this by setting the cell selectionStyle (seen in my solution below) and by implementing the tableView(_:willSelectRowAt) delegate method (also below.)

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

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

    let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell",
                                             for: indexPath) as! ItemCell
    cell.nameLabel.text = "No more items!"
    cell.serialNumberLabel.text = ""
    cell.valueLabel.text = ""
    cell.selectionStyle = UITableViewCellSelectionStyle.none


    if (indexPath.row < itemStore.allItems.count) {
        let item = itemStore.allItems[indexPath.row]
        cell.nameLabel.text = item.name
        cell.serialNumberLabel.text = item.serialNumber
        cell.valueLabel.text = "$\(item.valueInDollars)"
        cell.selectionStyle = UITableViewCellSelectionStyle.default
    }

    return cell
}

override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    // Check for "No more items!" row
    if indexPath.row >= itemStore.allItems.count {
        return nil
    }
    return indexPath
}

Gold Challenge: Customizing the Table

One important point of mention when reusing UITableViewCell’s here: we need to set the specific font sizes for all conditions.

If we are setting the size to 20 for the item cells, we should set it to another (default) font size for the last cell too.

Else if we have a lot of items (try setting the item count so that the table scrolls a lot), then the last row will most likely reuse the cells used for the items, and will use font size 20.

I also figured out another way of setting specific font sizes:

  • use another cell in the storyboard. Give it a new reuse identifier. Then reuse cells of chosen identifier in cellForRowAtIndexPath.

Advantages:

  • We can set the font size (and other UI elements) in the storyboard
  • Less chance of missing code for setting interfaces per cell type
1 Like

Bronze Challenge: Sections

We need to be careful using computed properties and avoid repeated unneeded re-computations.

The computations for highValueItems and otherItems as done below:

… will be repeated for each item in the following code:

A better solution would be to compute the properties only once.

One option:
by computing it in the init() call for ItemStore

ItemStore.swift

class ItemStore {
    
    var allItems = [Item]()
    var itemsLessThanEqualTo50 = [Item]()
    var itemsGreaterThan50 = [Item]()
    
    @discardableResult func createItem() -> Item {
        let newItem = Item(random: true)
        allItems.append(newItem)
        
        return newItem
    }
    
    init() {
        for _ in 0..<5 {
            createItem()
        }
        
        itemsLessThanEqualTo50 = allItems.filter{ $0.valueInDollars <= 50 }
        itemsGreaterThan50 = allItems.filter{ $0.valueInDollars > 50 }
    }
}

and then accessing them in the ItemsViewController.

Another option:
by computing them in the didSet call for itemStore in ItemsViewController.

ItemsViewController.swift

var itemsLessThanEqualTo50 = [Item]()
var itemsGreaterThan50 = [Item]()
var itemStore: ItemStore! {
    didSet {
        itemsGreaterThan50 = itemStore.allItems.filter{ $0.valueInDollars > 50 }
        itemsLessThanEqualTo50 = itemStore.allItems.filter{ $0.valueInDollars <= 50 }
        
        tableView.reloadData()
    }
}
1 Like

This controversial issue. In the study example it doesn’t affect anything, but in real world example computation may be better as it reduces RAM usage twice in this case.

On the Gold Challenge, increased the font size with the following system font setting:

cell.textLabel?.font = UIFont.boldSystemFont(ofSize: 20)

Because I used a footer to display “No More Items,” (cheating) – I could set the row heights globally:

    tableView.rowHeight = 60

My approach to the background image doesn’t display well. The image repeats above the table in the status bar, but it repeats starting from the bottom of the image:

  tableView.backgroundColor = UIColor(patternImage: #imageLiteral(resourceName: "co2"))`