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