Sans Pareil Technologies, Inc.

Key To Your Business

Lab 7: Navigation using Segues


So far we have covered only applications that have a single screen. In most practical applications, a single screen is not sufficient, and we need to present different screens (scenes in storyboard parlance) to the user for different features of the application. Storyboards allow us to add the various scenes necessary for our application by dragging and dropping instances of View Controller or its sub classes and then configuring the workflow by which the scenes are displayed to the user.

When the application is run, scenes are pushed on to a view controller stack, and then popped from the stack as necessary to take the user back and forth between the various scenes in the application.

Create a new project named Lab7. Make sure that the UITests target is unchecked, as we will not be using any UI tests in this project.

Model

We will use a slightly modified version of the Person entity used in Lab2. We will also use a repository class that will encapsulate access to and storage for the Person entity. Create a new Swift file named person in the project.

import Foundation

struct Person {
  var firstName: String
  var middleName: String?
  var lastName: String
  var email: String?
  var age: Int16
  
  var name: String {
    get {
      return (middleName == nil || middleName!.isEmpty) ? "\(firstName) \(lastName)" :
      "\(firstName) \(middleName!) \(lastName)"
    }
    set(value) {
      let arr = value.components(separatedBy: " ")
      if arr.count == 2 {
        firstName = arr[0]
        lastName = arr[1]
        middleName = nil
      } else {
        firstName = arr[0]
        middleName = arr[1]
        lastName = arr[2]
      }
    }
  }
}

class PersonRepository {
  static let instance = PersonRepository()
  
  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) {
    var p = persons[index]
    p.firstName = person.firstName
    p.middleName = person.middleName
    p.lastName = person.lastName
    p.email = person.email
    p.age = person.age
    persons[index] = p
  }
  
  private init() {}
  
  private var persons = [Person]()
}

Unit Test

We will now write some unit tests to test the PersonRepository implementation. Edit the automatically generated lab7Tests.swift file as follows:

import XCTest
@testable import lab7

class lab7Tests: XCTestCase {
  private let repository = PersonRepository.instance
  private let person1 = Person(firstName:"User", middleName: nil, lastName:"One", email:"user@one.com", age: 10)
  private let person2 = Person(firstName:"User", middleName: nil, lastName:"Two", email:"user@two.com", age: 11)
  private let person3 = Person(firstName:"User", middleName: "M", lastName:"Three", email: nil, age: 12)

  override func setUp() {
    super.setUp()

    XCTAssertTrue(repository.save(person: person1), "Person1 not saved")
    XCTAssertTrue(repository.save(person: person2), "Person2 not saved")
    XCTAssertTrue(repository.save(person: person3), "Person3 not saved")

    XCTAssertFalse(repository.save(person: person1), "Person1 saved twice")
    XCTAssertFalse(repository.save(person: person2), "Person2 saved twice")
    XCTAssertFalse(repository.save(person: person3), "Person3 saved twice")
  }
  
  override func tearDown() {
    XCTAssertTrue(repository.remove(person: person1), "Person1 not removed")
    XCTAssertTrue(repository.remove(person: person2), "Person2 not removed")
    XCTAssertTrue(repository.remove(person: person3), "Person3 not removed")

    XCTAssertFalse(repository.remove(person: person1), "Person1 removed twice")
    XCTAssertFalse(repository.remove(person: person2), "Person2 removed twice")
    XCTAssertFalse(repository.remove(person: person3), "Person3 removed twice")
    super.tearDown()
  }
  
  func testByFirstName() {
    var arr = repository.by(firstName: person1.firstName)
    XCTAssertEqual(3, arr.count, "Not all persons with same firstName found")

    arr = repository.by(firstName: "blah")
    XCTAssertTrue(arr.isEmpty, "Person with invalid firstName found")
  }

  func testByEmail() {
    if let p = repository.by(email: person1.email!) {
      XCTAssertEqual(person1.email, p.email, "Person1 not found by email")
    } else {
      XCTFail("User with email \(person1.email!) not found")
    }

    if let p = repository.by(email: person2.email!) {
      XCTAssertEqual(person2.email, p.email, "Person2 not found by email")
    } else {
      XCTFail("User with email \(person2.email!) not found")
    }

    if let _ = repository.by(email: "blah") {
      XCTFail("User with invalid email address found")
    }
  }
  
  func testByLastName() {
    var arr = repository.by(lastName: person1.lastName)
    XCTAssertEqual(1, arr.count, "Person1 not found by lastName")

    arr = repository.by(lastName: person2.lastName)
    XCTAssertEqual(1, arr.count, "Person2 not found by lastName")

    arr = repository.by(lastName: person3.lastName)
    XCTAssertEqual(1, arr.count, "Person3 not found by lastName")

    arr = repository.by(lastName: "blah")
    XCTAssertTrue(arr.isEmpty)
  }

  func testUpdate() {
    if var p = repository.by(email: person1.email!) {
      p.age = 15
      XCTAssertFalse(repository.save(person: p), "Updating existing person returned true")
      if let p1 = repository.by(email: p.email!) {
        XCTAssertEqual(15, p1.age, "Person age not modified")
      } else {
        XCTFail("Unable to retrieved saved person with email \(person1.email!)")
      }
    } else {
      XCTFail("User with email \(person1.email!) not found")
    }
  }
}

View Scene

Edit the Main.storyboard file and layout the initial screen for the application as follows. Make sure that the text field components all have user interaction disabled, as we will use a different scene to allow editing the Person entity details.
Screen Shot 2017-10-10 at 13.17.25

Edit Scene

Add a new scene to the storyboard by dragging and dropping a View Controller from the Object library into the storyboard (to the right of the initial scene). Control+Drag a connection from the Edit button to the new scene. Select the option “Present Modally” when prompted. Note that most of the other options are not usable when we manually use segues as we are doing in this exercise.

Create a new Swift class that extends UIViewController named EditViewController. Set the View Controller for the second scene to the newly created EditViewController using the Identity Inspector pane. Add outlets similar to ViewController that will bind to the UI elements in its associated screen.

Layout the edit scene as below. We remove the label at the top that displayed the person’s name, and rename the button at the bottom to Save from Edit.
Screen Shot 2017-10-10 at 13.39.39
Add a IBAction function to the initial ViewController that will serve as the action executed when the edit scene returns back to the main scene.

@IBAction func updatePerson(segue: UIStoryboardSegue) {}


Control+Drag a connection from the Save button to the Exit button in the Edit View Controller header bar. This will popup a dialog with the option to select the updatePerson action function to bind to. This function will let us transfer data modified in the UI back to the main ViewController.

ViewController

Edit the ViewController class with outlets for all the text fields and the name label in the scene. We will also override the prepare(for segue: sender:) function which will be invoked just before the edit scene is displayed on screen. Note that we cannot directly set the outlets from this function, since the outlets would not have been initialised at this point in time.

class ViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    createPerson()
    if let person = repository.by(email: emailAddress) {
      name.text = person.name
      firstName.text = person.firstName
      middleName.text = person.middleName
      lastName.text = person.lastName
      email.text = person.email
      age.value = Float(person.age)
    }
  }

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

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let controller = segue.destination as! EditViewController
    controller.person = repository.by(email: emailAddress)
  }

  @IBAction func updatePerson(segue: UIStoryboardSegue) {
    let controller = segue.source as! EditViewController
    if var person = repository.by(email: emailAddress) {
      person.firstName = controller.firstName.text!
      person.middleName = controller.middleName.text
      person.lastName = controller.lastName.text!
      person.email = controller.email.text
      person.age = Int16(controller.age.value)
      let _ = repository.save(person: person)

      name.text = person.name
      firstName.text = person.firstName
      middleName.text = person.middleName
      lastName.text = person.lastName
      email.text = person.email
      age.value = Float(person.age)
    }
  }

  private func createPerson() {
    let person = repository.by(email: emailAddress)
    if person == nil {
      let p = Person(firstName:"User", middleName: nil, lastName:"One", email:emailAddress, age: 10)
      let _ = repository.save(person: p)
    }
  }

  private let repository = PersonRepository.instance
  private var emailAddress = "user@one.com"
  
  @IBOutlet weak var name: UILabel!
  @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!
}

EditViewController

The code for this view controller is very simple. We just need outlets for the UI elements, and a property that stores the person entity being edited.

class EditViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    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)
    }
  }

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

  @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!
  var person : Person? = nil
}