Lab 10: Table Views
In this lab we will start exploring table views. Table views are one of the most commonly used scenes in a typical smartphone application. We will use a slightly modified version of the PersonRepository class we have been using as the underlying datastore for the table view. In this session we will only cover adding new entries to a table view. We will cover editing and removing items in the next lab session.
- Create a new Single View template based application named lab10.
- Drag and drop a Table View Controller from the object library into the storyboard.
- Make the new table view controller scene the initial scene for the application.
- Embed the table view controller scene in a Navigation Controller stack.
- Add a Navigation Item (Persons) and a Bar Button Item (Add) to the navigation bar.
Storyboard
The storyboard will consist of the initial scene that displays persons that are stored in the person repository. We will add additional scenes for creating and adding new persons to the repository. We will use a scene to request user input and a confirmation scene similar to the ones in previous lab sessions.
Repository
Here is the modified PersonRepository class. The Person struct is the same as used in previous lab sessions.
class PersonRepository { static let instance = PersonRepository() //MARK: New code func at(index:Int) -> Person { return persons[index] } func count() -> Int { return persons.count } //MARK: Old code func by(firstName: String) -> [Person] { var result = [Person]() for person in persons { if person.firstName == firstName { result.append(person) } } return result } func by(lastName: String) -> [Person] { var result = [Person]() for person in persons { if person.lastName == lastName { result.append(person) } } return result } func by(email: String) -> Person? { return persons.first(where: { $0.email == email }) } func save(person: Person) -> Bool { if let index = indexFor(name: person.name) { update(index: index, person: person) return false } if let email = person.email { if let index = indexFor(email: email) { update(index: index, person: person) return false } } persons.append(person) return true } func remove(person: Person) -> Bool { if let email = person.email { if let index = indexFor(email: email) { persons.remove(at: index) return true } } if let index = indexFor(name: person.name) { persons.remove(at: index) return true } return false } private func indexFor(name: String) -> Int? { return persons.index(where: { $0.name == name }) } private func indexFor(email: String) -> Int? { return persons.index(where: { $0.email == email }) } private func update(index: Int, person: Person) { persons[index] = person } private init() {} private var persons = [Person]() }
Table Controller
Add a new Cocoa Touch Class that inherits from UITableViewController named PersonTableViewController. We will auto generate a few person instances so that we have something to display in the table initially. We will then manually add a segue (Present Modally) to display the scene for creating a new person when the user selects the “Add” button.
import UIKit class PersonTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() createTestData() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } // 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 } //MARK: Actions @IBAction func addPerson(sender: UIStoryboardSegue) { if let controller = sender.source as? PersonSaveViewController, 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) } } //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") } } }
Table Cell
Create a new Cocoa Touch Class that inherits from TableViewCell named PersonTableViewCell. Create outlets for the three labels that we will use to display the person’s full name, email address and age in the custom table cell
import UIKit class PersonTableViewCell: UITableViewCell { override func awakeFromNib() { super.awakeFromNib() // Initialization code } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) } //MARK: Outlets @IBOutlet weak var name : UILabel! @IBOutlet weak var email : UILabel! @IBOutlet weak var age: UILabel! }
Select the Prototype Cells item displayed in the table view scene and assign the class to our custom class. Drag and drop three labels into the cell and lay them out as in the storyboard screen capture.
Create
Rename the automatically generated ViewController class to PersonCreateViewController. We will use it to control the scene used to capture user input for creating new person instances.
Embed the scene for creating a person into its own Navigation Controller stack. Add a Navigation Item (Create) and two Bar Button items (Cancel and Save) to the navigation bar. Connect the Add button in the table view scene to this scene using Present Modally option.
Embed the scene for creating a person into its own Navigation Controller stack. Add a Navigation Item (Create) and two Bar Button items (Cancel and Save) to the navigation bar. Connect the Add button in the table view scene to this scene using Present Modally option.
import UIKit class PersonCreateViewController: UIViewController, UITextFieldDelegate { override func viewDidLoad() { super.viewDidLoad() saveButton.isEnabled = false } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } //MARK: UITextFieldDelegate func textFieldDidBeginEditing(_ textField: UITextField) { saveButton.isEnabled = false } func textFieldDidEndEditing(_ textField: UITextField) { let text = email.text ?? "" saveButton.isEnabled = !text.isEmpty } //MARK: Actions @IBAction func cancel() { dismiss(animated: true, completion: nil) } override func shouldPerformSegue(withIdentifier identifier: String?, sender: Any?) -> Bool { if let emailAddress = email.text, let _ = repository.by(email: emailAddress) { NSLog("A person with specified email address: \(emailAddress) exists") email.becomeFirstResponder() return false } return true } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { super.prepare(for: segue, sender: sender) if let emailAddress = email.text, let controller = segue.destination as? PersonSaveViewController { let fn = firstName.text ?? "" let ln = lastName.text ?? "" controller.person = Person(firstName: fn, middleName: middleName.text, lastName: ln, email: emailAddress, age: Int16(age.value)) } } //MARK: Outlets @IBOutlet weak var saveButton: UIBarButtonItem! @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! private let repository = PersonRepository.instance }
Save
Create a new custom view controller class named PersonSaveViewController. Add a scene for displaying the confirmation screen to the storyboard as in earlier labs.
In the scene, bind the Done button to the PersonTableViewController.addPerson segue action function.
In the scene, bind the Done button to the PersonTableViewController.addPerson segue action function.
import UIKit extension NSAttributedString { internal convenience init?(html: String) { guard let data = html.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil } guard let attributedString = try? NSMutableAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else { return nil } self.init(attributedString: attributedString) } } class PersonSaveViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() display() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } //MARK: Outlets @IBOutlet weak var textView: UITextView! var person : Person? = nil //MARK: Internals private func display() { if let p = person { let middleName = p.middleName != nil ? p.middleName! : "" let email = p.email != nil ? p.email! : "" let html = """ <div> <label>First Name:</label> <label>\(p.firstName)</label><br/> <label>Middle Name:</label> <label>\(middleName)</label><br/> <label>Last Name:</label> <label>\(p.lastName)</label><br/> <label>Email:</label> <label>\(email)</label><br/> <label>Age:</label> <label>\(p.age)</label> </div> <br/><br/> <div>Click <b>Save</b> button to save this information, or the <b>Edit</b> button to go back and re-edit the information.</div> """ let attrStr = NSAttributedString(html: html) textView.attributedText = attrStr } } }