Swift Introduction/SwiftAdvanced

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Swift Advanced[edit]

Closure Expressions[edit]

Closure expressions are unnamed, anonymous functions which can interact with values from their surrounding contextref name="error">Apple Inc. | 2017 | Closure Expressions | [online][accessed: 18.09.2017] | https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID94</ref>. They are short function-like constructs without a declaration or name. In this snippet, you can see the general syntax of a closure expression. It starts with opening curly brackets, a parameter list and a return value. They keyword in marks the beginning of the closure's body. After the return statement the expression is closed with a curly bracket.

// basic syntax
let add = {(int1: Int, int2: Int) -> Int in return int1 + int2}
var result = add(20,5)      // 25

If a closure is assigned to a function type, a shorthand syntax can be used because Swift can infer the data types from the context. In the shortest version, you only have to tell Swift what to do with the parameters. $0 is short syntax for the first passed in argument, $1 for the second, and so on.

let subtract: (Int, Int) -> Int = { int1, int2 in return int1 - int2 }

let multiply: (Int, Int) -> Int = { $0 * $1 }

result = subtract(20,5)     // 15
result = multiply(20,5)     // 100

Trailing Closures[edit]

Closures can be passed to functions as arguments. A trailing closure can be used as the function's final argument. It's not written within the functions parentheses but right after them. In the snippet below, you can see how a closure is passed to Swift's map function. The map function can be called on an array, the closure is then executed on every item of the array. In this example, the map functions checks whether a year is a leapyear or not. All years are converted to a string and " is a leap year" is appended if the prerequisites are fulfilled.

// create an Array with values from 1950 to 2020
var years = [Int]()
for year in stride(from: 1950, to: 2020, by: 1){
    years.append(year)
}

let leapYears = years.map{ (year) -> String in
    var output = ""
    let year = year
    if(year%400 == 0){
        output.append(String(year)+" is a leap year")
    }
    else if(year % 100 != 0 && year % 4 == 0){
        output.append(String(year)  + " is a leap year")
    }
    else{
        output.append(String(year) + " is not a leap year")
    }
    return output
}

for year in leapYears{
    print(year)
}

Properties[edit]

In Swift, there are two main types of properties. Stored properties are used to store values of variables and constants associated with a class or a structure. Computed properties are not used to store, but to compute values.

Stored Properties[edit]

These properties are a part of an instance of a class or a structure. It can have a variable or a constant value.

struct Animal{
    let name: String
    let legs: Int
    var weight: Double
}

var sloth = Animal(name: "slothy", legs: 4, weight: 8.5)
print("Hi my name is \(sloth.name) and i have \(sloth.weight) kilos!")

sloth.weight = 9.2
print("Put one some weight... now i have \(sloth.weight) kilos :) !")

Computed Properties[edit]

These properties provide getters and setters to retrieve or set the value of a variable. In this example, the structure Circle has a computed property called diameter. It has a getter which returns the doubled value of the radius. The setter changes the value of the radius to the half of the new diameter. area is a read-only property, which means it does not have a setter.

struct Circle {
    var radius: Double
    var diameter: Double{
        get {
            let diameter = radius * 2
        return diameter
        }
        set(newDiameter){
            radius = newDiameter/2
        }
    }
    var area: Double{
        get{
            return radius * radius * Double.pi
        }
    }
}

var smallCircle = Circle(radius: 3)

let initialDiameter = smallCircle.diameter
smallCircle.diameter = 10
print("The circle now has a diameter of \(smallCircle.diameter), a radius of \(smallCircle.radius) and a area of \(smallCircle.area)")
// prints "The circle now has a diameter of 10.0, a radius of 5.0 and a area of 78.53"

Property Observers[edit]

Property observers can be used to observe the state of a property and respond to changes. They are called whenever the value of a property is set. There are two types of observers. willSet is called before a value is stored, didSet is called after a value is stored.

class EctsCounter{
    var ectsCount: Int = 0{
        willSet(newCount){
            print("About to set your count to \(newCount) points!")
        }
        didSet{
            print("Added \(ectsCount - oldValue) points!")
        }
    }
}

let counter = EctsCounter()
counter.ectsCount += 10
// About to set your count to 10 points!
// Added 10 points!
counter.ectsCount += 4
// About to set your count to 14 points!
// Added 4 points!

Concurrency with GCD[edit]

The dispatch framework includes a lot of language features, runtime libraries and system enhancements which improve the support for concurrent code execution on hardware with more than one core. The Grand Central Dispatch (GCD) submittes work to dispatch queues managed by the system.

Concurrency vs. Parallelism[edit]

Parallelism is the term which describes the concept of executing two or more threads at the same time on multi-core processors. Devices with a single core can achieve concurrency with time-slicing. This is the process of switching between multiple threads through context switches. GCD manages a pool of threads. Codeblocks can be added to dispatch queue and the GCD decides what to execute.

Queues[edit]

DispatchQueue represents the dispatch queues provided by GCD[1]. The tasks, which you submit to the queues, are executed in a FIFO order, which means the first submitted task will always be the first one that gets started. There are two types of queues. Serial queues can only execute one task at any time. Concurrent queues can start multiple tasks at the same time. They are started in the order they were added and can finish in any order. The scheduling is managed by GCD, which means it controls when tasks are started. GCD provides a main queue, which is a serial queue and runs on the main thread. Tasks on the main queue are executed immediatly. It is good practice to put all tasks, which change or update the UI, on the main queue. This makes sure the UI is always responsive. The four global queues are concurrent, shared by the entire system and devided in priorities - high, default, low and background. Queues can also be created by the user and can be serial or concurrent.

The priority is not specified directly, but by using Quality of Service (QoS) classes.

.user-interactive describes tasks which have to be done immediatly, because otherwise the user experience would be bad. This QoS is used for UI updates and event handling.

.user-initiated represents tasks, which can be performed asynchronously and are started from the UI. These tasks are mapped into the high priority queue, because it is used when users are waiting for immediate results or tasks require user interaction to continue.

.utility represents long-running tasks. This class is used for I/O, networking and computations. Usually a progress indicator is used to make sure the user knows something is going on.

.background describes tasks, which the user usually is not aware of. It is used for tasks that do not need user interaction and are not time-sensitive, for example maintenance or prefetchting.

Using queues in iOS[edit]

The following snipped can be found on Github and can be imported as Xcode project. As soon as the "Start" button is pressed, the function download(size: Int, label: UILabel, timeout: Int) is called twice with different input parameters. After they are put onto a global queue they asynchronously execute the simulation. After each iteration the amount is increased by 1. Next, the label which displays the progress of the download has to be updated. To do this, it is necessary to put the task back on the main queue, which makes sure that it is executed immediatly. After a short timeout the next iteration starts.

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var task1: UILabel!
    @IBOutlet weak var task2: UILabel!
    
    @IBAction func start(_ sender: UIButton) {
        download(size: 70, label: task1, timeout: 10)
        download(size: 50, label: task2, timeout: 7)
    }
    
    func download(size: Int, label: UILabel, timeout: Int) -> Void{
    
        DispatchQueue.global(qos: .userInitiated).async {
            // puts the download simulation on a global queue
            var amount = 0
            for _ in stride(from: 0, to: size, by: 1){
                amount += 1
                
                DispatchQueue.main.async {
                /* All actions which change the UI have to be put back on the main queue. */
                    label.text = String(amount)
                }
                // sets a timeout
                usleep(useconds_t(timeout*10000))
            }
        }
    }
}

Error Handling[edit]

There are a lot of situations where errors can occure during the execution of your code. For example, when trying to read a file from your harddisk errors can occur due to missing permission or non-existing files. Swift provides couple of ways[2] to deal with errors during code execution.

  • Errors in a function can be propagated to the code calling the function
  • do-catch statements
  • optional values
  • assert the error won't occur

Propagating Errors with throwing Functions[edit]

The keyword throws is used in a function's declaration after the parameterlist. This function is then called a "throwing function". In the example below, an enum is used to define two types of possible errors. Whenever the makeCoffee() function is called, the number of beans in the machine is decreased and a counter is increased. As soon as the beans are empty, an outOfBeans error is thrown. If a certain number of cups was served, the needsMaintenance error is thrown.

enum CoffeeMachineError: Error {
    case outOfBeans
    case needsMaintenance
}

class CoffeeMachine{
    var beans = 20
    var count = 1
    
    func makeCoffee() throws{
        if(count < 6){
            
            if(beans > 0){
                print("Enjoy your cup of coffee!")
                beans -= 1
                count += 1
            } else{
                throw CoffeeMachineError.outOfBeans
            }
        } else{
            throw CoffeeMachineError.needsMaintenance
        }
    }
}
var machine = CoffeeMachine()
for _ in stride(from: 0, to: 7, by: 1){
    try machine.makeCoffee()
}

Do-Catch[edit]

Do-Catch statements are used to execute different statements depending on what type of error is thrown. For example, here a needsMaintenance error is caught, the machine asks for maintenance and the counter is set back to 0.

var coffeeMachine = CoffeeMachine()
for run in stride(from: 0, to: 25, by: 1){
    do{
        try coffeeMachine.makeCoffee()
    } catch CoffeeMachineError.outOfBeans{
        print("Out of Beans!")
		
    } catch CoffeeMachineError.needsMaintenance{
        print("Machine needs Maintenance!")
        // Machine is maintained, counter gets set back to 0
        coffeeMachine.count = 0
    }
}

Converting Errors to Optional Values[edit]

With the try? keyword, an error is converted to an optional value. The value of an expression is nil whenever an error is thrown during the execution. In the snippet below, an error is thrown as soon as the digit that should be returned is no longer greater than zero.

enum DigitError: Error{
    case outOfDigitsError(String)
}

var currentDigit = 9
func  getDigit() throws -> Int {
    if(currentDigit > 0){
        let tmp = currentDigit
        currentDigit -= 1
        return tmp
    }
    else{
        throw DigitError.outOfDigitsError("Sorry, no digits left...")
    }
}

for _ in stride(from: 0, to: 10, by: 1){
    if let digit = try? getDigit(){
        print(digit)
    }
}


Accessing data from Sensors[edit]

Apple's mobile devices include a lot of sensors, including an Accelerometer, a Barometer, an Ambient light sensor and a Pedometer. iOS developers can access these sensors data in their projects with Swift.

Pedometer[edit]

As an example, the snippet below shows how the Pedometer can be accessed and how data can be retrieved[3]. This sensor is used for instance to count a persons steps. This project can be downloaded from GitHub.

import UIKit
import CoreMotion

@IBDesignable
class ViewController: UIViewController {
   
    
    @IBOutlet weak var stepsDisplay: UILabel!
    @IBOutlet weak var stepsToComplete: UILabel!
    
    let calendar = Calendar.current
    let todayDate = Date()
    var stepsToday: Int = 0
    let pedometer = CMPedometer()
    
    @IBInspectable
    var targetSteps = 10000
    
    func getStartOfDay(from date: Date) -> Date{
        return Calendar.current.startOfDay(for: date)
    }
    
    func handler (_ data: CMPedometerData?, _ error: Error?) -> Void{
        let steps = data?.numberOfSteps
        stepsToday = steps as! Int
        DispatchQueue.main.async(execute: {
            // puts the closure into the Main Queue so it will be executed immediatly
            self.stepsDisplay.text = String(self.stepsToday)
            self.stepsToComplete.text = String(self.getStepsToGoal(target: self.targetSteps, actual: self.stepsToday))
        })   
    }
    
    func getStepsToGoal(target steps: Int, actual count: Int) -> Int{
        return steps - count
    }
    
    @IBAction func getSteps(_ sender: UIButton){
        if CMMotionActivityManager.isActivityAvailable(){
            //checks if Activity Data is available
            pedometer.queryPedometerData(from: getStartOfDay(from: todayDate), to: todayDate, withHandler: handler)
            //queries data from the pedometer.
        }
    }   
}


Unit Testing[edit]

In this section we will have a look at how simple unit tests can be implemented in Swift[4]. For this purpose a simple class with three functions will be tested. The class contains variables for two integer values and three functions which can add, subtract or multiply those values.

import Foundation

class Calculator {
    
    var a: Int
    var b: Int
    
    init(a:Int, b:Int){
        self.a = a
        self.b = b
     }
    
    func add(a:Int, b:Int) -> Int {
        return a + b
    }
    
    func sub(a:Int, b:Int) -> Int {
        return a - b
    }
    
    func mul(a:Int, b:Int) -> Int {
        return a * b
    }
}

Next, let's have a look at how this class can be tested. First of all we have to import the XCTest testing framework and the Calculator. In the test functions testAdd(), testSub() and testMul() an instance of the Calculator class is used to call the methods add, subtract and multiply to compare the result to an expected value.

import XCTest
@testable import Calculator

class CalculatorTests: XCTestCase {
    
    let calc = Calculator(a:0, b:0)
    
    override func setUp() {
        // called before every test method
        super.setUp()
    }
    
    override func tearDown() {
        // called at the end of every test method
        super.tearDown()
    }
    
    func testAdd() {
        XCTAssertEqual(calc.add(a: 1, b: 1), 2)
        XCTAssertEqual(calc.add(a: 1, b: 2), 3)
        XCTAssertEqual(calc.add(a: 5, b: 4), 9)
    }
    
    func testSub(){
        XCTAssertEqual(calc.sub(a: 5, b: 2), 3)
        XCTAssertEqual(calc.sub(a: 3, b: 3), 0)
        XCTAssertEqual(calc.sub(a: 6, b: 7), -1)
    }
    
    func testMul(){
        XCTAssertEqual(calc.mul(a: 2, b: 4), 8)
        XCTAssertEqual(calc.mul(a: 9, b: 9), 81)
        XCTAssertEqual(calc.mul(a: 0, b: 4), 0)
    }
}

References[edit]

  1. raywenderlich.com | 2017 | Grand Central Dispatch Tutorial | [online][accessed: 18.09.2017] | https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1
  2. Apple Inc. | 2017 | Error Handling | [online][accessed: 18.09.2017] | https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html#//apple_ref/doc/uid/TP40014097-CH42-ID508
  3. Apple Inc. | 2017 | [online][accessed: 18.09.2017] | https://developer.apple.com/documentation/coremotion/cmpedometer
  4. Nolan, G. (2017), Agile Swift: Swift Programming Using Agile Tools and Tech- niques, Springer Science+Business Media New York, New York