A Student's Guide to Software Engineering Tools & Techniques »

Swift

Authors: Ch'ng Ming Shin, Jiang Chunhui, Yong Zhi Yuan

Reviewers: Aaron Chong, Bryan Lew, Dickson Tan, Rachael Sim, Rahul Rajesh, Sam Yong, Tan Wang Leng, Vivek Lakshmanan, Wang Junming, Xiao Pu

Swift Overview

Swift is the main programming language used for iOS programming. Introduced in 2014 by Apple, Swift has more concise and more expressive syntax compared to its predecessor language Objective-C. Unlike most other software by Apple, Swift is open source.

One main attraction to learn Swift's is that iOS developers are well paid (example).

Swift syntax is not too different from other mainstream languages such as Python, Java or C++, which means switching to Swift is not difficult. On top of that, Swift also supports playgrounds, a feature that allows programmers to experiment with Swift code and see the results immediately, without the overhead of building and running an app.

Noteworthy Swift Features

Here are some noteworthy Swift features for you to get a feel of Swift.

Type Inference

Swift supports type inference whereby the compiler automatically deduces the type of a variable during compilation by examining the values assigned to it.

var str1: String = "foo"
var str2 = "foo" // compiler infers that str2 is of type String

Unlike Python or JavaScript which are dynamically typed, variables in Swift are statically typed. Statically-typed languages have will have the code checked at compile-time instead of run-time, which eliminates many (often) trivial bugs early, which in turn makes debugging the program easier.

var str2 = "foo" // compiler infers that str2 is of type String
str2 = 5 // compilation error

Optionals

Swift allows the use of Optionals, so that you can choose to either return nil or a data value, instead of returning a special value to indicate the absence of a value.

func yearAlbumReleased(name: String) -> Int {
    switch name {
    case "Taylor Swift": return 2006
    case "Fearless": return 2008
    case "Speak Now": return 2010
    case "Red": return 2012
    case "1989": return 2014
    default:
        return -1 // Special value, but is not meaningful to other developers
    }
}

Without Optionals, you might consider using -1 to indicate that there was no such album. However, if someone else uses this function, he may not know that -1 means "no such album", and it would be better if we could return nil?

func yearAlbumReleased(name: String) -> Int? { // "?" indicates the return value type is an optional.
    switch name {
    case "Taylor Swift": return 2006
    case "Fearless": return 2008
    case "Speak Now": return 2010
    case "Red": return 2012
    case "1989": return 2014
    default:
        return nil
    }
}

Optional Binding

Then, you can unwrap the Optional safely using an if-let statement to distinguish whether it is nil or not, and handle them separately:

func timeTravel(album: String) {
    let year = yearAlbumReleased(album)
    if let past = year { 
        // year contains a non-nil value
        // past is of type Int (not Int?) with the value stored in year
    } else { 
        // year contains a nil value
    }
}

If you would like to read up more about optionals, take a look at these articles:

Guard Statements

Notice that the happy path in the code above is indented:

func timeTravel(album: String) {
    let year = yearAlbumReleased(album)
    if let past = year {
        // happy path is indented
        // past contains the non-nil value of year; proceed to do something with past
    } else { 
        // failure case
    }

    // past is no longer defined; unable to use past here
}

With the guard statement, the happy path is not indented:

func timeTravel(album: String) {
    let year = yearAlbumReleased(album)
    guard let past = year else {
        // failure case
        return
    }

    // happy path is not indented
    // past contains the non-nil value of year; proceed to do something with past
    // past remains defined till the function exits
}

Let's understand how the code above works:

  1. The code within a guard block is only executed if year contains a nil value.
  2. As the guard statement is used to transfer program control out of a scope, you must call one of the following functions within the guard block: return, break, continue, throw. As such, the guard statement is meant to enforce the pre-conditions of a method and to perform early return.

Here are some of the benefits of using guard statement over if-let statement:

  1. Unlike the if-let statement, using the guard statement causes past to remain defined and can be used till the function exits.
  2. While using if-let statements can lead to deeply nested if-let statements (i.e. pyramid of doom), guard statements allow us to have the happy path to be not indented, thereby increasing code readability.

Defer Statements

The defer key word in Swift provides an easy and safe way to execute some code before leaving current scope. It is helpful when you need to do post-operations in a function which has many points of return.

The following code is an example of using a file:

let fileDescriptor = open(url.path, O_EVTONLY)
if fileDescriptor == -1 {
	close(fileDescriptor)
	return "Failed"
}

// Use file descriptor

close(fileDescriptor)
return "Success"

As you can see from above, we have to close the fileDescriptor for every case we consider. This can be problematic when the number of cases increases. Instead, we can use the defer statement:

let fileDescriptor = open(url.path, O_EVTONLY)

defer {
    close(fileDescriptor)
}

if fileDescriptor == -1 {
	return "Failed"
}
// Use file descriptor
return "Success"

Using defer statement, the file will be closed no matter which branch the program returns. It also has the added advantage of preventing the developer from forgetting to close the file in some cases.

This document explains more about defer statement in Swift.

Data Types

Structs

Apart from the classes (something you are familiar with if you have already learned languages like Java / Python) which you use for creating instances of Reference type, Swift also provides the use of Structs to create instances of Value type.

Let's use a simple example (from Apple's own blog on Swift) to illustrate the difference between Reference types (Classes) and Value types (Structs).

// Value type example
struct S { var data: Int = -1 }
var a = S()
var b = a                       // a is copied to b
a.data = 42                     // changes a, not b
print("\(a.data), \(b.data)")   // prints "42, -1\n"

// Reference type example
class C { var data: Int = -1 }
var x = C()
var y = x                       // x is copied to y
x.data = 42                     // changes the instance referred to by x (and y)
print("\(x.data), \(y.data)")   // prints "42, 42\n"

In the example above, when you assign a reference type variable to another (i.e. x and y) they both refer to the same memory space. Later when you modify one of the variables, the other one will refer to the new value too. This may not be the desired behavior in some cases. In those cases, value-type variables (e.g., a and b) can be used to avoid implicit data sharing.

You can think of Structs as a way to create instances that have their own unique copies of data, which can help to make things a lot easier.

If you wish to find out more, here is an article that explains the difference between the 2 types, as well as the benefits of value types and when to use them.

Enums

An enum is a data type that represents of a set of values. For example, we can use String to represent the possible types of a barcode. However, this allows us to assign invalid values to it:

var barcode = "qzCode" // supposed to be "qrCode", but we accidentally assigned an invalid value

As such, we create an enum to restrict the values that we can assign to a barcode.

enum Barcode {
    case upc
    case qrCode
}

var barcode = Barcode.qrCode 
barcode = Barcode.qzCode // compilation error

Swift's enums can have associated values. This enables you to store additional custom information along with each case value, and permits this information to vary each time you use that case in your code. For example, we can have an enum Barcode with case values upc and qrCode. We want to be able to distinguish within each value as each upc and qrCode can take on different values:

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
    
    func printCode() {
        switch self {
        case let .upc(numberSystem, manufacturer, product, check):
            print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
        case let .qrCode(productCode):
            print("QR code: \(productCode).")
        }
    }
}

let barcode1 = Barcode.qrCode("foo")
let barcode2 = Barcode.qrCode("bar")
barcode1.printCode() // prints "QR code: foo."
barcode2.printCode() // prints "QR code: bar."

Also, enums with associated values is not supported in languages such as Java, and using a workaround to implement enums with associated values results in code verbosity. Take a look at Swift's documentation on Enums for more information about enums.

Protocol Oriented Programming

The heart of Swift is Protocol Oriented Programming (POP). POP helps to solve the bloat that is sometimes caused by Object Oriented Programming (OOP) by using composition instead of inheritance for defining new classes based on existing classes.

Here's some code to serve as a brief introduction to POP:

First, we first define our protocols.

protocol Bird {
  var canFly: Bool { get }
}
 
protocol Flyable {
  var airspeedVelocity: Double { get }
}

Next, we define the structs that conform to the protocols above.

// Penguins can't fly ):
struct Penguin: Bird {
    let canFly = false
}

struct Eagle: Bird, Flyable {
    let canFly = true
    let airspeedVelocity = 160.0
}

And if you haven't noticed, protocols are extremely similar to interfaces in Java.

To understand more about POP, watching this WWDC 2015 talk is highly recommended.

Extensions

Extensions allow us to add new functionalities to an existing class, structure, enumeration, or protocol type. Suppose we have an Eagle struct:

struct Eagle {
    // some functionalities here
}

As development progresses, you realize that you now want Eagle to conform to Bird and Flyable protocols. Instead of editing the code in Eagle struct directly, we can use extensions to implement each protocol separately. Do take note that you cannot add stored properties in extensions. As such, canFly and airspeedVelocity have to be computed properties (for more information, see here):

struct Eagle {
    // we can leave the existing code here untouched
}

extension Eagle: Bird {
    var canFly: Bool {
        return true
    }
}

extension Eagle: Flyable {
    var airspeedVelocity: Double {
        return 160.0
    }
}

Extensions also allow us to define instance methods and type methods for types which you do not have access to the original source code. For example:

extension String { // String belongs to Swift Standard Library which we have no access to
    // This method is copied from: 
    // https://github.com/SwifterSwift/SwifterSwift/blob/master/Sources/Extensions/SwiftStdlib/StringExtensions.swift
    func isAlphabetic() -> Bool {
        let hasLetters = rangeOfCharacter(from: .letters, options: .numeric, range: nil) != nil
        let hasNumbers = rangeOfCharacter(from: .decimalDigits, options: .literal, range: nil) != nil
        return hasLetters && !hasNumbers
    }
}

var foo: String = "a1"
print(foo.isAlphabetic()) // prints "false"

To find out more about extensions, take a look at Swift's documentation on Extensions

Automatic Reference Counting

A few keywords unique to Swift are strong, weak and unowned, which have to do with Swift's way of memory management, Automatic Reference Counting (ARC).

Essentially, when an instance of a class is created, a chunk of memory is allocated to it until it is no longer strongly referenced by anything. References are strong by default. Thus, if we have an Object A (a UIViewController) that creates an Object B (a UIAlertController), B would be strongly referenced by A. However, B might also need access to a variable in A, such that A may be strongly referenced by B, resulting in a reference cycle.

Reference cycles are bad, because they cause memory leaks. Even though A and B are no longer needed eventually, A and B will still sit in memory since they are both strongly referenced by each other. This is why we need the strong, weak and unowned keywords, to resolve reference cycles.

Here is an article with greater in-depth explanation and examples.

CocoaPods

CocoaPods is a dependency manager for Swift and Objective-C Cocoa projects which has over 58 thousand libraries and is used in over 3 million apps. Instead of reinventing the wheel, you can check this out to obtain code that helps resolve common issues. If you have done something new with Swift, you can also make your code into a library with CocoaPods for others to use!

How to Get Started?

A Macbook is required for Swift development, but an iPhone or iPad is not. The Swift IDE X-Code has built-in simulators for all mobile devices.

If you have not learnt any other programming languages before, this Game App could be a good choice to learn swift as well as programming.

If you are familiar with some programming languages, reading the Language Guide in the official documentation is recommended, since it explains everything quite clearly, albeit being quite verbose.

If you are really pressed for time, here are a couple of cheatsheets with code examples: