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]() }
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.
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.
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.
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.
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 }