Sans Pareil Technologies, Inc.

Key To Your Business

Lab 2 - Introduction to Swift


In this exercise, we will build a simple application that represents details about a Person on screen. While developing this application, we will learn about Swift structures, classes, functions, String objects etc.

Project


Create a Single View Application named Lab2 following the same steps we used in Lab 1. We will add some Swift source code to the project first, and then build the user interface in the main storyboard file.

Person

We will first create a Swift structure named Person that we will use as the Model for our application. Structures are used to represent data and functions that operate on that data as a single re-usable unit. Structures are similar to classes, with the main difference being that structures are value based (stack based), while classes are pointer based. Structures are always passed by value, making them much easier and safer to use in multi-threaded environments.

Structures do not support inheritance in the traditional sense, however, structures can adopt (or conform to) protocols, which grant structures the same type of polymorphic abilities that classes have.

  • Use secondary mouse click on the Lab2 group within the Xcode project navigator pane and choose “New File …” to add a new file to our project.
  • Select Swift File as the template for the file.
  • Name the file Person.swift.
  • Add String properties for the parts of a person’s name (with middleName being an Optional String), and a computed property named name which will return the full name of the person, as well as set the parts of the person’s name using an input full name. We will also add a static (class) function that will return a random sequence of words.

import Foundation

struct Person {
  var firstName: String
  var middleName: String?
  var lastName: String
  
  var name: String {
    get {
      return middleName == nil ? "\(firstName) \(lastName)" :
    "\(firstName) \(middleName!) \(lastName)"
    }
    set(value) {
      let arr = value.components(separatedBy: " ")
      if arr.count == 2 {
        firstName = arr[0]
        lastName = arr[1]
      } else {
        firstName = arr[0]
        middleName = arr[1]
        lastName = arr[2]
      }
    }
  }
  
  init(firstName: String) {
    self.init(firstName: firstName, lastName: firstName)
  }
  
  init(firstName: String, lastName: String) {
    self.init(firstName: firstName, middleName: nil, lastName: lastName)
  }
  
  init(firstName: String, middleName: String?, lastName: String) {
    self.firstName = firstName
    self.middleName = middleName
    self.lastName = lastName
  }
  
  static func description(length: UInt8) -> String {
    func word() -> String {
      let base = "abcdefghijklmnopqrstuvwxyz"
      let arr = Array(base.characters)
      var value = ""
      
      for _ in 0..<length {
        value.append(arr[Int(arc4random()) % arr.count])
      }
      return value
    }
    
    var words = ""
    for i in 0..<length {
      words.append(word())
      if i < length - 1 { words.append(" ") }
    }
    
    return words
  }
}

Lab2Tests

Edit the automatically generated Lab2Tests unit test source file and add tests for the Person structure. We will test both the computed name property and the static description function.

import XCTest
@testable import Lab2

class Lab2Tests: XCTestCase {
  
  func testName() {
    var person = Person(firstName: "First", lastName: "Last")
    XCTAssertEqual("First Last", person.name, "Person name computed property without middleName")
    
    person.middleName = "Middle"
    XCTAssertEqual("First Middle Last", person.name, "Person name updated with middleName")
    
    person.name = "My New Name"
    XCTAssertEqual("My", person.firstName, "Person firstName updated via name setter")
    XCTAssertEqual("New", person.middleName, "Person middleName updated via name setter")
    XCTAssertEqual("Name", person.lastName, "Person lastName updated via name setter")
    
    let person2 = Person(firstName: "One", middleName: "Two", lastName: "Three")
    XCTAssertEqual( "One Two Three", person2.name, "Person name set via init")
    
    let person3 = Person(firstName: "Name")
    XCTAssertEqual("Name Name", person3.name, "Person name set via single arg init")
  }
  
  func testDescription() {
    
    func check(count: UInt8) {
      let description = Person.description(length: count)
      let arr = description.components(separatedBy: " ")
      XCTAssertEqual(Int(count), arr.count, "Description has \(count) words")
      
      for i in 0..<arr.count {
        XCTAssertEqual(Int(count), arr[i].characters.count, "Word \(arr[i]) has \(count) characters")
      }
    }
    
    check(count: 4)
    check(count: 7)
  }
}

Main.storyboard

Build a user interface that will represent the information in the Person structure. We will use text fields for the parts of the name and full name, a text view for the description and a stepper for controlling the number of characters and words generated for the description. We will use buttons to update the Person structure from the values in the UI.

For this exercise, we will manually write the outlets and actions in the view controller and then connect those to the components in the storyboard.

ViewController

Classes use dynamic memory (heap based) and are represented as references (really pointers) in Swift. Class references may be shared between various parts of the application code base, making it possible to modify data/state of the application from different code paths. Shared data need special handling when used from multi-threaded code. Most internal API’s are implemented in C/Objective-C making the use of classes necessary when working with UIKit or other Apple API’s.

Add code to the view controller source file and then connect the UI to the properties in controller.
  • Edit the automatically generated ViewController.swift file.
  • Add properties to represent the UI components.
  • Add a property for the Person structure.
  • Add action functions for the two buttons we will use to update the Person name.
  • Make sure that all text fields set the ViewController as the delegate (important for the keyboard to be dismissed after use).

import UIKit

class ViewController: UIViewController, UITextFieldDelegate {

  private var person: Person = Person(firstName: "")
  
  @IBOutlet var firstName: UITextField!
  @IBOutlet var middleName: UITextField!
  @IBOutlet var lastName: UITextField!
  @IBOutlet var name: UITextField!
  @IBOutlet var length: UIStepper!
  @IBOutlet var desc: UITextView!
  
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    return true
  }

  @IBAction func updateName() {
    NSLog("Update name of person from the full name field")
    person.firstName = firstName.text!
    person.middleName = middleName.text
    person.lastName = lastName.text!
    name.text = person.name
    updateDesc()
  }

  @IBAction func updatePartsOfName() {
    NSLog("Update parts of name using the full name field")
    person.name = name.text!
    firstName.text = person.firstName
    middleName.text = person.middleName
    lastName.text = person.lastName
    updateDesc()
  }
  
  private func updateDesc() {
    desc.text = Person.description(length: UInt8(length.value))
  }
}

Lab2UITests

Add our automated UI test code to the Xcode generated test file. Note that the hardware keyboard option needs to be turned off in the Simulator for the test framework to enter text into the text fields.
Screen Shot 2017-08-29 at 08.48.47

import XCTest

class Lab2UITests: XCTestCase {
  
  override func setUp() {
    super.setUp()
    continueAfterFailure = false
    XCUIApplication().launch()
  }
  
  override func tearDown() {
    super.tearDown()
  }
  
  func testName() {
    let firstName = XCUIApplication().textFields["firstName"]
    let middleName = XCUIApplication().textFields["middleName"]
    let lastName = XCUIApplication().textFields["lastName"]
    let name = XCUIApplication().textFields["name"]
    let description = XCUIApplication().textViews["description"]
    let incrementButton = XCUIApplication().steppers.buttons["Increment"]
    
    func test1() {
      setText(field: firstName, text: "One")
      setText(field: lastName, text: "Two")
      incrementButton.tap()
      incrementButton.tap()
      incrementButton.tap()
      
      XCUIApplication().buttons["updateName"].tap()
      XCTAssertEqual("One Two", name.value as! String)
      XCTAssert((middleName.value as! String).isEmpty)
      
      let text = description.value as! String
      let arr = text.components(separatedBy: " ")
      XCTAssertEqual(3, arr.count)
      for i in 0..<arr.count { XCTAssertEqual(3, arr[i].characters.count) }
    }

    func test2() {
      setText(field: middleName, text: "Two")
      setText(field: lastName, text: "Three")
      incrementButton.tap()
      incrementButton.tap()
      XCUIApplication().buttons["updateName"].tap()
      XCTAssertEqual("One Two Three", name.value as! String)
      
      let text = description.value as! String
      let arr = text.components(separatedBy: " ")
      XCTAssertEqual(5, arr.count)
      for i in 0..<arr.count { XCTAssertEqual(5, arr[i].characters.count) }
    }

    func test3() {
      setText(field: name, text: "My Full Name")
      XCUIApplication().buttons["updatePartsOfName"].tap()
      XCTAssertEqual("My", firstName.value as! String)
      XCTAssertEqual("Full", middleName.value as! String)
      XCTAssertEqual("Name", lastName.value as! String)
      
      let text = description.value as! String
      let arr = text.components(separatedBy: " ")
      XCTAssertEqual(5, arr.count)
      for i in 0..<arr.count { XCTAssertEqual(5, arr[i].characters.count) }
    }
    
    func test4() {
      XCUIApplication().steppers.buttons["Decrement"].tap()
      setText(field: name, text: "My Name")
      XCUIApplication().buttons["updatePartsOfName"].tap()
      XCTAssertEqual("My", firstName.value as! String)
      XCTAssert((middleName.value as! String).isEmpty)
      XCTAssertEqual("Name", lastName.value as! String)
      
      let text = description.value as! String
      let arr = text.components(separatedBy: " ")
      XCTAssertEqual(4, arr.count)
      for i in 0..<arr.count { XCTAssertEqual(4, arr[i].characters.count) }
    }
    
    test1()
    test2()
    test3()
    test4()
  }
  
  private func setText(field: XCUIElement, text: String) {
    let deleteKey = XCUIApplication().keys["delete"]
    field.tap()
    deleteKey.press(forDuration: 2.0)
    field.typeText(text)
    XCUIApplication().keyboards.buttons["return"].tap()
  }
}