Swift Learning (1) - First Encounter (Code Enhanced Version)

Swift Learning (1) - First Encounter (Code Enhanced Version)

August 4, 2021·Jingyao Zhang
Jingyao Zhang

image

Introduction to Swift

Swift is a programming language launched by Apple Inc., specifically designed for application development on Apple’s desktop operating system macOS and mobile operating systems iOS, iPadOS, as well as watchOS and tvOS. Swift surpasses Objective-C in many aspects, with fewer complex symbols and expressions. At the same time, Swift is faster, more convenient, efficient, and safer. In addition, the new Swift language remains compatible with Objective-C. (For more information about Swift, visit the official website)

Generally, a line of Swift code is a complete program.

print("Hello, World!")
---
output: Hello, World!

Simple Values

In Swift, constants are declared with let. A constant value can only be assigned once and can be used in multiple places.

let constValue = 20
let constInteger: Int
print(constValue)
---
output: 20

Variables in Swift are declared with var, similar to the syntax in JavaScript. Variables can be assigned multiple times.

var variableValue = 20
variableValue = 30
print(variableValue)
---
output: 30

The type of a constant or variable must match the value assigned to it, but you don’t have to explicitly declare the type. When declaring a constant or variable with a value, the compiler will infer its type automatically. If the initial value does not provide enough information, or if there is no initial value, you need to declare the type after the variable/constant, separated by a colon.

let implicitInteger = 70
print("Implicit Integer:", implicitInteger)
let implicitDouble = 71.0
print("Implicit Double:", implicitDouble)
let explicitDouble: Double = 72
print("Explicit Double:", explicitDouble)
---
output: Implicit Integer: 70
Implicit Double: 71.0
Explicit Double: 72.0

Practice:

Create a constant, explicitly specify its type as Float, and assign it an initial value of 4

Reference answer:

let explicitFloat: Float = 100
print("Practice Result:", explicitFloat)
---
output: Practice Result: 100.0

Values are never implicitly converted to other types. If you need to convert a value to another type, do so explicitly.

let label = "The width is "
let width = 94
let widthLabel = label + String(width)
print("Explicit Conversion Result:", widthLabel)
---
output: Explicit Conversion Result: The width is 94

Practice:

Remove String from the second to last line above. What is the error message?

Reference answer:

Binary operator '+' cannot be applied to operands of type 'String' and 'int'


There is a simpler way to convert a value to a string: write the value in parentheses, and precede the parentheses with a backslash \.

let apples = 3
let oranges = 5
let fruitSummary = "I have \(apples) apples, and \(oranges) oranges. So I have \(apples + oranges) pieces of fruits totaly."
print(fruitSummary)
---
output: I have 3 apples, and 5 oranges. So I have 8 pieces of fruits totaly.

Practice:

Use \() to convert a floating-point calculation to a string, add someone’s name, and greet them

Reference answer:

let constScore = 301.92
let nameSocre = "Jensen \(constScore)"
print("Hello, ", nameSocre)
---
output: Hello,  Jensen 301.92

Use triple double quotes """ to enclose multi-line string content. The indentation at the beginning of each line will be removed until it matches the indentation of the closing quotes.

let quotation = """
I said "I have \(apples) apples, and \(oranges) oranges"
And then I said "I have \(apples + oranges) pieces of fruits totaly."
"""
print(quotation)
---
output: I said "I have 3 apples, and 5 oranges"
And then I said "I have 8 pieces of fruits totaly."

Use square brackets [] to create arrays and dictionaries, and use subscripts or keys to access elements. A trailing comma is allowed after the last element.

var shoppingList = ["catfish", "water", "tulips", "blue paint", ]
print(shoppingList[0])
shoppingList[2] = "Jensen"
print(shoppingList)

var occupations = [
        "Country": "China",
        "City": "Hefei"
]
occupations["University"] = "HFUT"
print(occupations)
---
output: catfish
["catfish", "water", "Jensen", "blue paint"]
["University": "HFUT", "Country": "China", "City": "Hefei"]

Arrays automatically grow when elements are added.

shoppingList.append("apples")
print("Length after adding element to array:", shoppingList.count)
---
output: Length after adding element to array:  5

Use initializer syntax to create an empty array or dictionary.

let emptyArray: [String] = []
let emptyDicitionary: [String: Float] = [:]

If the type information of a variable can be inferred, you can use [] and [:] to create empty arrays and dictionaries.

shoppingList = []
occupations = [:]

Control Flow

Use if and switch for conditional operations, and use for-in, while, and repeat-while for loops. Parentheses around conditions and loop variables can be omitted. In if statements, the condition must be a Boolean expression, which means code like if score { ... } will result in an error and will not be implicitly compared to 0.

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
        if score > 50 {
                teamScore += 3
        } else {
                teamScore += 1
        }
}
print("The value of teamScore: ", teamScore)
---
output: The value of teamScore:  11

You can use if and let together to handle missing values. These values can be represented by optionals. An optional value is either a concrete value or nil to indicate a missing value. Add a question mark ? after the type to mark the variable as optional.

var optionalString: String? = "Hello"
print(optionalString == nil)
optionalString = nil
print(optionalString == nil)

var optionalName: String? = "Jensen Jon"
var greeting = "Hello"
if let name = optionalName {
        greeting = "Hello, \(name)"
        print(greeting)
}
---
output: false
true
Hello, Jensen Jon

Practice:

Change optionalName to nil. What will greeting be? Add an else statement to assign a different value to greeting when optionalName is nil.

Reference answer: If the optional value is nil, the condition will be false and the code in the braces will be skipped. If it is not nil, the value will be unwrapped and assigned to the constant after let, so the value can be used in the code block.


Another way to handle optionals is to use the ?? operator to provide a default value. If the optional value is missing, the default value is used instead.

let nickName: String? = nil
let fullName: String = "Jensen Jon"
let informalGreeting = "Hi \(nickName ?? fullName)!"
print("informalGreeting: ", informalGreeting)
---
output: informalGreeting:  Hi Jensen Jon!

switch supports any type of data and various comparison operations—not just integers and equality tests.

let vegetable = "red pepper"
switch vegetable {
case "celery":
        print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
        print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):
        print("Is it a spicy \(x)?")
default:
        print("Everything testes good in soup.")
}
---
output: Is it a spicy red pepper?

Practice:

Remove the default statement and see what error occurs.

Reference answer: Switch must be exhaustive


You can use for-in to iterate over a dictionary. You need a pair of variables to represent each key-value pair. Dictionaries are unordered collections, so their keys and values are iterated in arbitrary order.

let interestingNumbers = [
        "Prime": [2, 3, 5, 7, 11, 13],
        "Fibonacci": [1, 1, 2, 3, 5, 8],
        "Square": [1, 4, 9, 16, 25]
]
var largest = 0
for (_, numbers) in interestingNumbers {
        for number in numbers {
                if number > largest {
                        largest = number
                }
        }
}
print("The largest value in interestingNumbers dictionary is:", largest)
---
output: The largest value in interestingNumbers dictionary is:  25

Practice:

Replace _ with a variable name to determine which type has the largest value.

Reference answer:

var largCls = ""; largest = 0
for (varClass, numbers) in interestingNumbers {
        for number in numbers {
                if number > largest {
                        largest = number
                        largCls = varClass
                }
        }
}
print("The type with the largest value in the dictionary is:", largCls)
---
output: The type with the largest value in the dictionary is: Square

Use while to repeatedly run a piece of code until the condition changes. The loop condition can also be at the end, ensuring the loop runs at least once.

var n = 2
while n < 100 {
        n *= 2
}
print("n: ", n)

var m = 2
repeat {
        m *= 2
} while m < 100
print("m: ", m)
---
output: n:  128
m:  128

You can use ..< in loops to indicate a range of indices.

var total = 0
for i in 0..<5 {
        print(i)
        total += i
}
print("Total(..<): ", total)
---
output: 0
1
2
3
4
Total(..<):  10

A range created with ..< does not include the upper bound. If you want to include it, use ....

total = 0
for i in 0...5 {
        print(i)
        total += i
}
print("Total(...): ", total)
---
output: 0
1
2
3
4
5
Total(...):  15

Functions and Closures

Use func to declare a function, and use the name and parameters to call it. Use -> to specify the return type of the function.

func greet(person: String, day: String) -> String {
        return "Hello \(person), today is \(day)."
}
print(greet(person: "Jensen", day: "Thursday"))
---
output: Hello Jensen, today is Thursday.

Practice:

Remove the day parameter and add a parameter to the greeting to indicate today’s special dish.

Reference answer:

func greet(person: String, delicacy: String) -> String {
        return "Hello \(person), our special delicacy is \(delicacy) today!"
}
print(greet(person: "Jensen", delicacy: "beef"))
---
output: Hello Jensen, our special delicacy is beef today!

By default, functions use their parameter names as argument labels. You can customize argument labels before the parameter name, or use _ to omit the argument label.

func greet(_ person: String, on day: String) -> String {
        return "Hello \(person), today is \(day)."
}
print(greet("Jensen", on: "Thursday"))
---
output: Hello Jensen, today is Thursday.

Use tuples to generate compound values, such as returning multiple values from a function. The elements of the tuple can be accessed by name or number.

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
        var min = scores[0]
        var max = scores[0]
        var sum = 0
        
        for score in scores {
                if score > max {
                        max = score
                } else if score < min {
                        min = score
                }
                sum += score
        }

        return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, -9, 0])
print("Max value is ", statistics.max, "\nMin value is ", statistics.min, "\nSum value is ", statistics.sum)
print(statistics.2)
---
output: Max value is  100 
Min value is  -9 
Sum value is  99
99

Functions can be nested, and nested functions can access variables from the outer function. You can use nested functions to refactor a function that is too long or complex.

func returnFifteen() -> Int {
        var y = 10
        func add() {
                y += 5
        }
        add()
        return y
}
print("Value of y after nested function:", returnFifteen())
---
output: Value of y after nested function:  15

Functions are first-class types, which means a function can be returned from another function.

func makeIncrementer() -> ((Int) -> Int) {
        func addOne(number: Int) -> Int {
                return 1 + number
        }
        return addOne
}
var increment = makeIncrementer()
print("Increment is ", increment(7))
---
output: Increment is  8

Functions can also be passed as parameters to other functions.

func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
        for item in list {
                if condition(item) {
                        return true
                }
        }
        return false
}
func lessThanTen(number: Int) -> Bool {
        return number < 10
}
var numbers = [10, 20, 9, 2, 11]
print(hasAnyMatches(list: numbers, condition:  lessThanTen))
---
output: true

Functions are actually a special kind of closure: code that can be called later. The code in a closure can access variables and functions in the closure’s scope, even if the closure is executed in a different scope, as in the nested function above. You can use {} to create an anonymous closure, and use in to separate the parameter and return type declarations from the closure body.

var result = numbers.map({
        (number: Int) -> Int in
        let result = 3 * number
        return result
})
print("Closure value:", result)
---
output: Closure value: [30, 60, 27, 6, 33]

Practice:

Rewrite the closure to return 0 for all odd numbers

Reference answer:

result = numbers.map({
        (number: Int) -> Int in
        if number % 2 != 0 {
                return 0
        }
        return number
})
print("Closure value:", result)
---
output: Closure value: [10, 20, 0, 2, 0]

If the type of a closure is known, such as when used as a delegate callback, you can omit the parameters, return value, or both. Single-statement closures return the value of their statement as the result.

let mappedNumbers = numbers.map({number in 3 * number})
print("Concise closure:", mappedNumbers)
---
output: Concise closure: [30, 60, 27, 6, 33]

You can refer to parameters by their position rather than by name. This is useful in very short closures. When a closure is the last argument to a function, it can be placed directly after the parentheses. When a closure is the only argument to a function, the parentheses can be omitted entirely.

let sortedNumbers = numbers.sorted {$0 > $1}
print("Concise closure (no parentheses):", sortedNumbers)
---
output: Concise closure (no parentheses): [20, 11, 10, 9, 2]

Classes and Objects

Use class and a class name to create a class. Properties in a class are declared the same way as variables and constants, except their context is the class. Similarly, methods are declared the same way as functions.

class Shape {
        var numberOfSides = 0
        func simpleDescription() -> String {
                return "A shape with \(numberOfSides) sides."
        }
}
print(Shape().simpleDescription())
---
output: A shape with 0 sides.

Practice:

Use let to add a constant property, and add a method that takes a parameter

Reference answer:

class Shapes {
        let constVal = 10
        var numberOfSize = 20
        func setter(_ number: Int) {
                numberOfSize = number
        }
        func shwoInfo() -> String {
                return "Class Shape has a const value constVal and a variable value numberOfSize, the value of constVal is \(constVal), the value of numberOfSize is \(numberOfSize)."
        }
}
var shapes = Shapes()
shapes.setter(100)
print(shapes.shwoInfo())
---
output: Class Shape has a const value constVal and a variable value numberOfSize, the value of constVal is 10, the value of numberOfSize is 100.

To create an instance of a class, add parentheses after the class name. Use dot syntax to access the instance’s properties and methods.

var shape = Shape()
shape.numberOfSides = 7
print("shape object created:", shape.simpleDescription())
---
output: shape object created: A shape with 7 sides.

The Shape/Shapes class above is missing a very important thing: a constructor to initialize instances. Use init to create an initializer.

class NamedShape {
        var numberOfSides: Int = 0
        var name: String
        
        init(name: String) {
                self.name = name
        }
        
        func simpleDescription() -> String {
                return "A shape with \(numberOfSides) sides."
        }
}

Note, similar to Python, in the example above self is used to distinguish the instance variable name from the initializer parameter name. When you need to create an instance, you need to pass parameters to the class’s initializer as you would to a function. When creating an instance, every property of the class must be assigned a value, either through declaration or the initializer.

If you need to do some cleanup before an object is released, use deinit to create a destructor.

To define a subclass, add the parent class name after the subclass name, separated by a colon. When creating a class, you don’t need a standard root class, so you can add or omit a parent class as needed. If a subclass needs to override a method from its parent class, use the override keyword to mark it. If you override without adding override, the compiler will report an error.

class Square: NamedShape {
        var sideLength: Double
        
        init(sideLength: Double, name: String) {
                self.sideLength = sideLength
                super.init(name: name)
                numberOfSides = 4
        }
        
        func area() -> Double {
                return sideLength * sideLength
        }
        
        override func simpleDescription() -> String {
                return "A square with sides of length \(sideLength). Producted by \(name)."
        }
}
let test = Square(sideLength: 5.2, name: "Jensen")
print("Square: Area", test.area())
print(test.simpleDescription(), test.numberOfSides)
---
output: Square: Area 27.040000000000003
A square with sides of length 5.2. Producted by Jensen. 4

Practice:

Create another subclass of NamedShape called Circle. The initializer takes two parameters: one for the radius and one for the name. Implement area() and simpleDescription() methods in the subclass.

Reference answer:

class Circle: NamedShape {
        var radius: Double
        
        init(radius: Double, name: String) {
                self.radius = radius
                super.init(name: name)
                numberOfSides = 1
        }
        
        func area() -> Double {
                return radius * radius * Double.pi
        }

        override func simpleDescription() -> String {
                return "A circle has \(numberOfSides) side and with radius of \(radius), its area is \(area()). Producted by \(name)."
        }
}
let circle = Circle(radius: 5.2, name: "Jensen")
print(circle.simpleDescription())
---
output: A circle has 1 side and with radius of 5.2, its area is 84.94866535306801. Producted by Jensen.

In addition to simple stored properties, there are computed properties with getter and setter.

class EquilateralTriangle: NamedShape {
        var sideLength: Double
        
        init(sideLength: Double, name: String) {
                self.sideLength = sideLength
                super.init(name: name)
                numberOfSides = 3
        }
        
        var perimeter: Double {
                get {
                        return 3.0 * sideLength
                }
                set {
                        sideLength = newValue  / 3.0
                }
        }
        
        override func simpleDescription() -> String {
                return "An equilateral triangle with sides of length \(sideLength)."
        }
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "Jensen")
print(triangle.simpleDescription())
triangle.perimeter = 9.9
print("New value after setter:", triangle.sideLength)
---
output: An equilateral triangle with sides of length 3.1.
New value after setter: 3.3000000000000003

In the setter of perimeter, the name of the new value is newValue. You can explicitly set a name in parentheses after set. Note that the initializer of the EquilateralTriangle class performs three steps:

Sets the property values declared by the subclass

Calls the parent class’s initializer

Changes the property values defined by the parent class. Other work, such as calling methods, getters, and setters, can also be done at this stage.

If you don’t need a computed property but still want to run code before or after setting a new value, use willSet and didSet. The code you write will be called when the property value changes, but not when the value changes in init.

class TriangleAndSquare {
        var triangle: EquilateralTriangle {
                willSet {
                        square.sideLength = newValue.sideLength
                }
        }
        
        var square: Square {
                willSet {
                        triangle.sideLength = newValue.sideLength
                }
        }
        
        init(size: Double, name: String) {
                square = Square(sideLength: size, name: name)
                triangle = EquilateralTriangle(sideLength: size, name: name)
        }
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "Jensen")
print("First call > Square.sideLength:", triangleAndSquare.square.sideLength)
print("First call > EquilaterTriangle.sideLength:", triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 30, name: "Jensen")
print("Second call > EquilaterTriangle.sideLength:", triangleAndSquare.triangle.sideLength)
---
output: First call > Square.sideLength: 10.0
First call > EquilaterTriangle.sideLength: 10.0
Second call > EquilaterTriangle.sideLength: 30.0

When dealing with optional values, you can add ? before operations (such as methods, properties, and subscripts). If the value before ? is nil, everything after ? is ignored and the whole expression returns nil. Otherwise, the optional value is unwrapped and all subsequent code runs as if the value was unwrapped. In both cases, the value of the whole expression is also an optional.

var optionalSquare: Square? = Square(sideLength: 2.5, name: "Jensen")
var sideLength = optionalSquare?.sideLength
print("Has optional value:", sideLength)
optionalSquare = nil
sideLength = optionalSquare?.sideLength
print("No optional value:", sideLength)
---
output: Has optional value: Optional(2.5)
No optional value: nil
---
warning: Expression implicitly coerced from 'Double?' to 'Any'

Enums and Structs

Use enum to create an enumeration. Like classes and all other named types, enums can contain methods.


enum RankTest: Double {
        case ace
        case two, three, four, five, six, seven
}
print("Enum test:", RankTest.two.rawValue)

enum Rank: Int {
        case ace = 1
        case two, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king
        
        func simpleDescription() -> String {
                switch self {
                case .ace:
                        return "ace"
                case .jack:
                        return "jack"
                case .queen:
                        return "queen"
                case .king:
                        return "king"
                default:
                        return String(self.rawValue)
                }
        }
}
let ace = Rank.ace
let aceRawValue = ace.rawValue
print("ace: ", aceRawValue)
print("Rank.xx.simpleDescription(): ", Rank.king.simpleDescription())
---
output: Enum test: 1.0
ace: 1
Rank.xx.simpleDescription(): king

Practice:

Write a function to compare two Rank values by comparing their raw values

Reference answer:

enum Rank1: Int {
        case ace = 10
        case two, three
}

enum Rank2: Int {
        case ace = 7
        case next
}

func compareRanks(rankVal1: Int, rankVal2: Int) -> String {
        if rankVal1 < rankVal2 {
                return "Rank1"
        } else if rankVal2 < rankVal1 {
                return "Rank2"
        } else {
                return "Null"
        }
}

print("The Rank with the larger raw value is:", compareRanks(rankVal1: Rank1.ace.rawValue, rankVal2: Rank2.ace.rawValue))
---
output: The Rank with the larger raw value is: Rank2

By default, Swift assigns raw values starting from 0 and incrementing by 1. Raw values can be modified by explicit assignment. You can also use strings or floating-point numbers as enum raw values, and use the rawValue property to access the raw value of an enum member.

Use the init?(rawValue:) initializer to create an enum instance from a raw value. If there is an enum member corresponding to the raw value, it returns that member; otherwise, it returns nil.

if let convertedRank = Rank(rawValue: 11) {
        let elevenDescription = convertedRank.simpleDescription()
        print(elevenDescription)
}
---
output: jack

Associated values of enums are actual values, not just another way of expressing raw values. In fact, if there are no meaningful raw values, don’t provide raw values.

enum Suit {
        case spaeds, hearts, diamonds, clubs
        func simpleDescription() -> String {
                switch self {
                case .spaeds:
                        return "speedsDescription"
                case .hearts:
                        return "heartsDescription"
                case .diamonds:
                        return "diamondsDescription"
                case .clubs:
                        return "clubsDescription"
                }
        }
}
let hearts = Suit.hearts
print("Suit.hearts: ", hearts)
let heartsDescription = hearts.simpleDescription()
print("heartsDescription: ", heartsDescription)
---
output: Suit.hearts: hearts
heartsDescription: heartsDescription

Practice:

Add a color() method to Suit. Return “black” for spades and clubs, and “red” for hearts and diamonds.

Reference answer:

enum SuitTest {
        case spaeds, hearts, diamonds, clubs
        func simpleDescription() -> String {
                switch self {
                case .spaeds:
                        return "speedsDescription"
                case .hearts:
                        return "heartsDescription"
                case .diamonds:
                        return "diamondsDescription"
                case .clubs:
                        return "clubsDescription"
                }
        }
        
        func color() -> String {
                switch self {
                case .spaeds:
                        return "black"
                case .clubs:
                        return "black"
                case .hearts:
                        return "red"
                case .diamonds:
                        return "red"
                }
        }
}
let clubs = SuitTest.clubs
print("Suits.clubs: ", clubs)
let clubsColor = clubs.color()
print("clubsColor: ", clubsColor)
---
output: Suits.clubs: clubs
clubsColor: black

Note that in the example above, two ways are used to refer to the hearts enum member:

When assigning to the clubs constant, the enum member Suits.clubs needs to be referenced by its full name because the constant does not have an explicit type.

In the switch, the enum member is referenced by the shorthand .clubs because the value of self is already of type Suits.

You can use the shorthand in any context where the variable type is known.

If an enum member instance has a raw value, the value is determined at declaration time, which means different instances of the enum member will always have the same raw value. Of course, you can also set associated values for enum members. Associated values are determined when the instance is created, which means different instances of the same enum member can have different associated values.

enum ServerResponse {
        case result(String, String)
        case failure(String)
}

var response = ServerResponse.result("6:00 am", "8:09 pm")
response = ServerResponse.failure("Out of memory.")

switch response {
case let .result(sunrise, sunset):
        print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .failure(message):
        print("Failure... \(message)")
}
---
output: Failure... Out of memory.

Practice:

Add a third case to ServerResponse and switch

Reference answer:

enum ServerResponses {
        case result(String, String)
        case rain(String)
        case failure(String)
}

var responses = ServerResponses.result("6:00 am", "8:09 pm")
responses = ServerResponses.failure("Out of memory.")
responses = ServerResponses.rain("Today is rain.")

switch responses {
case let .result(sunrise, sunset):
        print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .rain(message):
        print("Sorry! \(message)")
case let .failure(message):
        print("Failure... \(message)")
}
---
output: Sorry! Today is rain.

Use struct to create a structure. Structs and classes have many similarities, including methods and initializers. The biggest difference is that structs are value types, while classes are reference types.

struct Card {
        var rank: Rank
        var suit: Suit
        func simpleDescription() -> String {
                return "The \(rank.simpleDescription()) of \(suit.simpleDescription())."
        }
}
let threeOfSpades = Card(rank: .three, suit: .spaeds)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()
print("Struct value passing:", threeOfSpadesDescription)
---
output: Struct value passing: The 3 of speedsDescription.

Practice:

Write a method to create a complete deck of cards, which are all combinations of rank and suit

Reference answer:

enum Ranks: CaseIterable {
        case ace
        case two, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king
}
enum Suits: CaseIterable {
        case spades, clubs, hearts, diamonds
}

struct Cards {
        var rank: Ranks
        var suit: Suits
}

func allCards() {
        for suit in Suits.allCases {
                for rank in Ranks.allCases {
                        Cards(rank: rank, suit: suit)
                }
        }
}

Protocols and Extensions

Use protocol to declare a protocol.

protocol ExampleProtocol {
        var simpleDescription: String { get }
        mutating func adjust()
}

Classes, enums, and structs can all conform to protocols, similar to interfaces in Java.

class SimpleClass: ExampleProtocol {
        var simpleDescription: String = "A very simple class."
        var anotherProperty: Int = 69105
        func adjust() {
                simpleDescription += " Now 100% adjusted."
        }
}
var a = SimpleClass()
a.adjust()
print("Adjusted Class's Description: ", a.simpleDescription)

struct SimpleStructure: ExampleProtocol {
        var simpleDescription: String = "A simple structure."
        mutating func adjust() {
                simpleDescription += " (adjusted)"
        }
}
var b = SimpleStructure()
b.adjust()
print("Adjusted Struct's Description: ", b.simpleDescription)
---
output: Adjusted Class's Description:  A very simple class. Now 100% adjusted.
Adjusted Struct's Description:  A simple structure. (adjusted)

Practice:

Add another requirement to ExampleProtocol. What changes are needed in SimpleClass and SimpleStructure to ensure they still conform to the protocol?

Reference answer: Add a method to SimpleClass and SimpleStructure to implement the requirement.


Note that when declaring SimpleStructure, the mutating keyword is used to mark a method that modifies the struct. The declaration of SimpleClass does not need to mark any methods, because methods in classes can usually modify the class’s properties.

Use extension to add functionality to existing types, such as new methods and computed properties. You can use extensions to make a type declared elsewhere conform to a protocol, which also applies to types imported from external libraries or frameworks.

extension Int: ExampleProtocol {
        var simpleDescription: String {
                return "The number \(self)"
        }
        mutating func adjust() {
                self += 0
        }
}
print("Extension > Int: ", 7.simpleDescription)
---
output: Extension > Int:  The number 7

Practice:

Write an extension for Double to add a roundValue method

Reference answer:

protocol DoubleProtocol {
        func roundValue() -> Int
}

extension Double: DoubleProtocol {
        func roundValue() -> Int {
                return Int((self).rounded())
        }
}
print("Extension > Double: ", (3.14).roundValue())
---
output: Extension > Double:  3

You can use protocol names like any other named type. For example, create a collection of objects of different types that all implement a protocol. When dealing with values whose type is a protocol, methods defined outside the protocol are not available.

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
// print(protocolValue.anotherProperty)  // Uncommenting this line will cause an error
---
output: A very simple class. Now 100% adjusted.

Even if the runtime type of the protocolValue variable is SimpleClass, the compiler will treat its type as ExampleProtocol, which means you cannot call methods or properties outside the protocol.


Error Handling

Use types that adopt the Error protocol to represent errors.

enum PrinterError: Error {
        case outofPaper
        case noToner
        case onFire
}

Use throw to throw an error and throws to indicate a function can throw errors. If an error is thrown in a function, the function returns immediately and the calling code handles the error.

func send(job: Int, toPrinter printerName: String) throws -> String {
        if printerName == "Never has Toner" {
                throw PrinterError.noToner
        }
        return "\(printerName), Job sent."
}
print("ErrorDemo: ", try send(job: 10, toPrinter: "Okay"))
---
output: ErrorDemo:  Okay, Job sent.

There are several ways to handle errors. One way is to use do-catch. In the do block, use try to mark code that can throw errors. In the catch block, unless otherwise named, the error is automatically named error.

do {
        let printerResponse = try send(job: 1040, toPrinter: "Jensen")
        print(printerResponse)
} catch {
        print(error)
}
---
output: Jensen, Job sent.

Practice:

Change printerName to “Never has Toner” to make the send(job:toPrinter:) function throw an error

Reference answer:

do {
        let printerResponse = try send(job: 0, toPrinter: "Never has Toner")
        print(printerResponse)
} catch {
        print(error)
}
---
output: noToner

You can use multiple catch blocks to handle specific errors, written in the style of case in switch.

do {
        let printerResponse = try send(job: 0, toPrinter: "Never has Toner")
        print(printerResponse)
} catch PrinterError.onFire {
        print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
        print("Printer error: \(printerError)")
} catch {
        print(error)
}
---
output: Printer error: noToner

Practice:

Add code that throws an error in the do block. Which error should be thrown to make the first catch block handle it? How can the second and third catch blocks be triggered?

Reference answer: To trigger the first catch block, throw the onFire error; to trigger the second, throw any PrinterError other than onFire; to trigger the third, throw a non-PrinterError error.


Another way to handle errors is to use try? to convert the result to an optional. If the function throws an error, the error is discarded and the result is nil. Otherwise, the result is an optional containing the function’s return value.

let printerSuccess = try? send(job: 1884, toPrinter: "Jensen is handsome")
let printerFailure = try? send(job: 1885, toPrinter: "Never has Toner")
print("printerSuccess: \(printerSuccess)\nprinterFailure: \(printerFailure)")
---
output: printerSuccess: Optional("Jensen is handsome, Job sent.")
printerFailure: nil
---
warning: String interpolation produces a debug description for an optional value; did you mean to make this explicit?

Use the defer block to indicate code that will be executed last before the function returns. This code will be executed whether or not the function throws an error. With defer, you can write code that should be executed at the beginning of a function and cleanup code that should be executed at the end together, even though their execution times are different.

var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]

func fridgeContains(_ food: String) -> Bool {
        fridgeIsOpen = true
        defer {
                fridgeIsOpen = false
        }
        
        let result = fridgeContent.contains(food)
        return result
}
print("Do this fridge contains banana? ", fridgeContains("banana"))
print("FridgeIsOpen: ", fridgeIsOpen)
---
output: Do this fridge contains banana?  false
FridgeIsOpen:  false

Generics

Write a name in angle brackets to create a generic function or type.

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
        var result: [Item] = []
        for _ in 0..<numberOfTimes {
                result.append(item)
        }
        return result
}
let repeatNum: [String] = makeArray(repeating: "Test", numberOfTimes: 5)
print(repeatNum)
---
output: ["Test", "Test", "Test", "Test", "Test"]

You can also create generic functions, methods, classes, enums, and structs.

// (Reimplementing the Optional type in Swift standard library)
enum OptionalValue<Wrapped> {
        case none
        case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)
print(possibleInteger)
---
output: some(100)

Use where after the type name to specify a series of requirements for the type, such as requiring the type to implement a protocol, requiring two types to be the same, or requiring a class to have a specific superclass.

func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
where T.Element: Equatable, T.Element == U.Element
{
        for lhsItem in lhs {
                for rhsItem in rhs {
                        if lhsItem == rhsItem {
                                return true
                        }
                }
        }
        return false
}
print("WHERE Test: ", anyCommonElements([1, 2 ,3], [3]))
---
output: WHERE Test:  true

Practice:

Modify the anyCommonElements(_ :_ :) function to create a function that returns an array containing the common elements of two sequences

Reference answer:

func anyCommonElementsInArray<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> [T.Element]
where T.Element: Equatable, T.Element == U.Element
{
        var outArray: [T.Element] = []
        for lhsItem in lhs {
                for rhsItem in rhs {
                        if lhsItem == rhsItem {
                                outArray.append(lhsItem)
                        }
                }
        }
        return outArray
}
print("CommonArray: ", anyCommonElementsInArray([0, 1, 2, 3, 4], [0, 3, 4, 5, 6]))
---
output: CommonArray:  [0, 3, 4]

<T: Equatable> and <T> ... where T: Equatable are equivalent.


This post is just an overview of the Swift language. More detailed tutorials on Swift will be shared in the future.

Last updated on