Here is my solution. Since all the challenges build on top of each other this is combined solution.
The two included code sources show two approaches that can be taken in order to solve the challenges. The first approach uses sections, and the second accomplishses the task by using some tableView apis.
Approach #1: Sections (feels hacky)
import UIKit
class ItemsViewController: UITableViewController {
var itemStore: ItemStore!
@IBAction func addNewItem(_ sender: UIButton) {
let newItem = itemStore.createItem()
if let index = itemStore.allItems.index(of: newItem) {
let indexPath = IndexPath(row: index, section: 0)
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 viewDidLoad() {
super.viewDidLoad()
let statusBarHeight = UIApplication.shared.statusBarFrame.height
let insets = UIEdgeInsets(top: statusBarHeight, left: 0, bottom: 0, right: 0)
tableView.contentInset = insets
tableView.scrollIndicatorInsets = insets
}
// I chose to create two sections to make the constant cell easier
override func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
// I switched over the section to create artifial cells
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return itemStore.allItems.count
default:
return 1
}
}
// When creating cells I also switched over the section
// I chose not to make the cell reusable since it's only one cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case 1:
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
cell.textLabel?.text = "No more items!"
return cell
default:
let item = itemStore.allItems[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = "$\(item.valueInDollars)"
return cell
}
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete, indexPath.section < 1 {
let item = itemStore.allItems[indexPath.row]
let title = "Remove \(item.name)?"
let message = "Are you sure you want to remove 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: "Remove", style: .destructive, handler: {(action) -> Void in
self.itemStore.removeItem(item)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
})
ac.addAction(deleteAction)
present(ac, animated: true, completion: nil)
}
}
// sets the editing style of a cell
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
switch indexPath.section {
case 1:
return .none
default:
return .delete
}
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row)
}
// prevents cells from moving to different sections
override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
if sourceIndexPath.section != proposedDestinationIndexPath.section {
return sourceIndexPath
}
return proposedDestinationIndexPath
}
}
Approach #2 Using UITableView Api’s (the difference may seem subtle but in my opinion the code feels idiomatic.)
class ItemsViewController: UITableViewController {
var itemStore: ItemStore!
@IBAction func addNewItem(_ sender: UIButton) {
let newItem = itemStore.createItem()
if let index = itemStore.allItems.index(of: newItem) {
let indexPath = IndexPath(row: index, section: 0)
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 viewDidLoad() {
super.viewDidLoad()
let statusBarHeight = UIApplication.shared.statusBarFrame.height
let insets = UIEdgeInsets(top: statusBarHeight, left: 0, bottom: 0, right: 0)
tableView.contentInset = insets
tableView.scrollIndicatorInsets = insets
}
// return itemStore items count + one for last item
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemStore.allItems.count + 1
}
// When creating cells I also switched over the section
// I chose not to make the cell reusable since it's only one cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row < itemStore.allItems.count {
let item = itemStore.allItems[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = "$\(item.valueInDollars)"
return cell
} else {
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
cell.textLabel?.text = "No more items!"
cell.isUserInteractionEnabled = false
cell.textLabel?.isEnabled = false
return cell
}
}
// clause to check if row is last row is not needed because
// editing is only enabled for rows within the itemStore count
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let item = itemStore.allItems[indexPath.row]
let title = "Remove \(item.name)?"
let message = "Are you sure you want to remove 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: "Remove", style: .destructive, handler: {(action) -> Void in
self.itemStore.removeItem(item)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
})
ac.addAction(deleteAction)
present(ac, animated: true, completion: nil)
}
}
// disable editing if the row is larger the items count
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
if indexPath.row < itemStore.allItems.count {
return true
}
return false
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row)
}
// prevents cells from moving to different sections
override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
if proposedDestinationIndexPath.row < itemStore.allItems.count {
return proposedDestinationIndexPath
}
return sourceIndexPath
}
}