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