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