Sans Pareil Technologies, Inc.

Key To Your Business

Lab 11: Editing Table Views


In this lab session, we will update the project from lab10 to add support for editing and deleting Person entities displayed in the table view. Since we re-use view controllers that display details of a Person entity in Create/Detail/Edit scenes, we will also use a base controller class that implements most of the common properties and functions used by the target view controllers.

You can either modify project lab10, or copy it to a new directory and edit it. Renaming a project is a slightly involved process, and we will not cover it in this course. We will also use the same PersonSaveViewController for the final confirmation scene in the edit workflow.

Storyboard

Add view controllers for detail, edit and save scenes as done in lab8. Unlike the scenes for Create/Confirm, we will attach the edit scenes directly to the Navigation Controller for the Table View.
Screen Shot 2017-11-06 at 20.29.59

Base Controller

Create a new Cocoa Touch class that inherits from UIViewController named PersonViewController. In this class we will add the usual outlets and a function that binds the UI components from the Person entity.

import UIKit

class PersonViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
  }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }

  func bindPerson() {
    if let p = person {
      firstName.text = p.firstName
      middleName.text = p.middleName
      lastName.text = p.lastName
      email.text = p.email
      age.value = Float(p.age)
    }
  }

  //MARK: Outlets
  @IBOutlet weak var firstName: UITextField!
  @IBOutlet weak var middleName: UITextField!
  @IBOutlet weak var lastName: UITextField!
  @IBOutlet weak var email: UITextField!
  @IBOutlet weak var age: UISlider!
  
  let repository = PersonRepository.instance
  var person : Person!
}

Detail Controller

Create a UIViewController subclass named PersonDetailViewController. Once the controller class is created, update it to make it inherit from PersonViewController instead of UIViewController. We will also override the bindPerson function as this controller has an additional outlet for the computed name property of the person.

import UIKit

class PersonDetailViewController: PersonViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    bindPerson()
  }
  
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }
  
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let controller = segue.destination as! PersonEditViewController
    controller.person = person
  }
  
  override func bindPerson() {
    super.bindPerson()
    if let p = person {
      name.text = p.name
    }
  }
  
  @IBOutlet weak var name: UILabel!
}

Edit Controller

Create another subclass of UIViewController named PersonEditViewController. Modify it also to inherit from PersonViewController.

import UIKit

class PersonEditViewController: PersonViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    bindPerson()
  }
  
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let controller = segue.destination as? PersonSaveViewController else {
      fatalError("Unexpected destination: \(segue.destination)")
    }

    if var p = person {
      p.firstName = firstName.text!
      p.middleName = middleName.text
      p.lastName = lastName.text!
      p.age = Int16(age.value)
      controller.person = p
    }
  }

}

Table Controller

We will update the PersonTableViewController to add support for editing rows. Note that we need to add the pre-configured editButtonItem through code, and not by dragging and dropping a standard Bar Button Item to the navigation bar.

import UIKit

class PersonTableViewController: UITableViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    navigationItem.leftBarButtonItem = editButtonItem
    createTestData()
  }
  
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
  }
  
  // MARK: - Table view data source
  
  override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
  }
  
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if section == 0 {
      return repository.count()
    }
    return 0
  }

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cellIdentifier = "PersonCell"

    guard let cell = tableView.dequeueReusableCell(
      withIdentifier: cellIdentifier, for: indexPath) as? PersonTableViewCell else {
        fatalError("Unable to cast cell to PersonTableViewCell")
    }

    let person = repository.at(index: indexPath.row)
    cell.name.text = person.name
    cell.email.text = person.email
    cell.age.text = "\(person.age)"

    return cell
  }

  override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
  }

  override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
      let person = repository.at(index: indexPath.row)
      if !repository.remove(person: person) {
        fatalError("Could not remove person with email: \(String(describing: person.email))")
      }
      tableView.deleteRows(at: [indexPath], with: .fade)
    }
  }

  // MARK: - Navigation

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    super.prepare(for: segue, sender: sender)
    if "PersonEdit" != (segue.identifier ?? "") {
      return
    }

    guard let controller = segue.destination as? PersonDetailViewController else {
      fatalError("Unexpected destination: \(segue.destination)")
    }
    
    guard let cell = sender as? PersonTableViewCell else {
      fatalError("Unexpected sender: \(sender ?? "unknown")")
    }
    
    guard let indexPath = tableView.indexPath(for: cell) else {
      fatalError("The selected cell is not being displayed by the table")
    }

    let person = repository.at(index: indexPath.row)
    controller.person = person
  }

  //MARK: Actions
  @IBAction func addPerson(sender: UIStoryboardSegue) {
    if let controller = sender.source as? PersonSaveViewController {
      if let person = controller.person {
        if !repository.save(person: person) {
          fatalError("Unable to save new person")
        }
      }
      let indexPath = IndexPath(row: repository.count() - 1, section: 0)
      tableView.insertRows(at: [indexPath], with: .automatic)
    }
  }
  
  @IBAction func updatePerson(sender: UIStoryboardSegue) {
    if let controller = sender.source as? PersonSaveViewController {
      if let person = controller.person {
        if repository.save(person: person) {
          fatalError("Created duplicate person with email: \(String(describing: person.email))")
        }
      }
      if let selectedIndexPath = tableView.indexPathForSelectedRow {
        tableView.reloadRows(at: [selectedIndexPath], with: .none)
      }
    }
  }
  
  //MARK: Private interface
  private let repository = PersonRepository.instance
  
  private func createTestData() {
    if !repository.save(person: Person(firstName:"User", middleName: nil, lastName:"One", email:"user@one.com", age: 10)) {
      fatalError("Unable to create first test user")
    }
    if !repository.save(person: Person(firstName:"User", middleName: nil, lastName:"Two", email:"user@two.com", age: 11)) {
      fatalError("Unable to create second test user")
    }
    if !repository.save(person: Person(firstName:"User", middleName: "M", lastName:"Three", email: "user@three.com", age: 12)) {
      fatalError("Unable to create third test user")
    }
  }
}