A Swift Tour

 

iOS Development with Swift programming language

Colab Note

print("Hello, world!")
Hello, world!

Syntax:

  1. Input / output and string handlings are builtin and do not require importing anything
  2. do not need the main()function
  3. Do not require semicolons ;

Simple Values

  1. use let to make a constant type. constant type can be assigned exactly once.
  2. use var to make a variable. should have the same type if we want to reassign value on it.
  3. when we create a constant or variable with initial value, we do not need to write its type, cause the compiler can infer its type.
var myVar = 42
myVar = 50
let myConst = 3.14

If we do not have initial values, or explicitly want specify its type, then we need to specify its type:

let explicitFloat = 4

Converting values to different type

let label = "The width is "
let width = 94

let widthWithLabel = label + String(width)


print(widthWithLabel)
The width is 94

String interpolation

print("Desk width is \(width)")
Desk width is 94

Multi-line String

let quotation = """
Nobody ever figures out what life is all about, and it doesn't matter.
      Study hard what interests you the most in the most undisciplined, irreverent and original manner possible.
You have no responsibility to live up to what other people think you ought to accomplish.
"""

print(quotation)
Nobody ever figures out what life is all about, and it doesn't matter.
      Study hard what interests you the most in the most undisciplined, irreverent and original manner possible.
You have no responsibility to live up to what other people think you ought to accomplish.

Arrays

var fruits = ["🍏","🍎","🍐"]

print(fruits[1])
🍎

Add elements to array

fruits.append("🍊")

print(fruits)
["🍏", "🍎", "🍐", "🍊"]

Create Empty Array

let emptyArray : [String] = []

Dictionary

var occupations = [
    "Feynman" : "Physicist",
    "Jobs" : "Entertainer"
]
print(occupations["Feynman"])

Add elements to Dictionary

occupations["Musk"] = "Scammer"
print(occupations)
["Musk": "Scammer", "Jobs": "Entertainer", "Feynman": "Physicist"]

Create an Empty Dict

let emptyDict : [String:Float] = [:]

Control Flow

for ... in

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
    if score > 50 {
        teamScore += 3
    } else {
        teamScore += 1
    }
}
print(teamScore)
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(largest)
25
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
var belongsTo : String = ""
for (group, numbers) in interestingNumbers {
    for number in numbers {
        if number > largest {
            largest = number
            belongsTo = group
        }
    }
}
print("largest is \(largest) in group:\(belongsTo)")
largest is 25 in group:Square
var total = 0
for i in 0..<4 {
    total += i
}
print(total)
6

if ... let woring with optionals

var optionalString: String? = "Hello"
print(optionalString == nil)
false
var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
    greeting = "Hello, \(name)"
}
print(greeting)
Hello, John Appleseed
var optionalName: String? = nil
var greeting = "Hello!"
if let name = optionalName {
    greeting = "Hello, \(name)"
}
print(greeting)
Hello!
let nickname: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickname ?? fullName)"

print(informalGreeting)
Hi John Appleseed

Switch

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 tastes good in soup.")
}

while

Use while to repeat a block of code until a condition changes.

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

var m = 2
repeat {
    m *= 2
} while m < 100
print(m)

Functions and Closures

Declaring a function with func

Use func to declare a function.

func greet(person: String, day: String) -> String {
    return "Hello \(person), today is \(day)."
}
greet(person: "Bob", day: "Tuesday")
"Hello Bob, today is Tuesday."
func greet(person: String, special: String) -> String {
    return "Hello \(person), today's special is \(special)."
}
greet(person: "Bob", special: "Lagman")
"Hello Bob, today\'s special is Lagman."

Function with Positional arguments

Or we can use positional arguments,

func greet(_ person: String, on day: String) -> String {
    return "Hello \(person), today is \(day)."
}
greet("John", on: "Wednesday")

_ made it positional

Function return compound value with Tuple

Use a tuple to make a compound value

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, 3, 9])
print(statistics.sum)
print(statistics.2)

Functions can be nested

func returnFifteen() -> Int {
    var y = 10
    func add() {
        y += 5
    }
    add()
    return y
}
returnFifteen()

15

Functions are a first-class type

Functions are a first-class type. This means that:

A function can return another function as its value.

func makeIncrementer() -> ((Int) -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}
var increment = makeIncrementer()
increment(7)
8

A function can take another function as one of its arguments.

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 = [20, 19, 7, 12]
hasAnyMatches(list: numbers, condition: lessThanTen)
true

Functions are special case of closures

Functions are actually a special case of closures: blocks of code that can be called later.

numbers.map({ (number: Int) -> Int in
    let result = 3 * number
    return result
})
▿ 4 elements
  - 0 : 60
  - 1 : 57
  - 2 : 21
  - 3 : 36
numbers.map({ (number: Int) -> Int in
    let result = 0
    if number % 2 == 0 { return result }
    return number
})
▿ 4 elements
  - 0 : 0
  - 1 : 19
  - 2 : 7
  - 3 : 0

When a closure’s type is already known, such as the callback for a delegate, you can omit the type of its parameters, its return type, or both.

Single statement closures implicitly return the value of their only statement.

let mappedNumbers = numbers.map({number in 3 * number})
print(mappedNumbers)
[60, 57, 21, 36]

You can refer to parameters by number instead of by name

let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)

Objects and Classes

Defining a Class

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

Add a constant property with let, and add another method that takes an argument.

class Shape {
    var numberOfSides = 0
    let pi = 3.14

    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }

    func sayHello(name:String) -> String {
        return "Hello \(name)!"
    }
}

Creating the class instance

var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()

Class Initializer init

class NamedShape {
    var numberOfSides: Int = 0
    var name: String

    init(name:String){
        self.name = name
    }

    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

Use deinit to create a deinitializer if you need to perform some cleanup before the object is deallocated.

Class inheritance (Subclass)

Subclasses include their superclass name after their class name, separated by a colon.

Methods on a subclass that override the superclass’s implementation are marked with override.

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)."
    }
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()
"A square with sides of length 5.2."

Make another subclass of NamedShape called Circle that takes a radius and a name as arguments to its initializer. Implement an area() and a simpleDescription() method on the Circle class.

class Cirlce: NamedShape {
    var radius: Double

    init(radius: Double, name: String) {
        self.radius = radius
        super.init(name: name)
    }

    func area() -> Double {
        return 2 * 3.14 * radius
    }

    override func simpleDescription() -> String {
        return "A square with radius \(radius)."
    }
}

let testCircle = Cirlce(radius : 4, name: "Test Circle")
testCircle.area()
25.12
testCircle.simpleDescription()
"A square with radius 4.0."

Getter and Setter

In addition to simple properties that are stored, properties can have a getter and a setter.

class EquilateralTriangle: NamedShape {
    var sideLength: Double = 0.0

    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: "a triangle")
print(triangle.perimeter)
triangle.perimeter = 9.9
print(triangle.sideLength)

willSet and didSet

If you don’t need to compute the property but still need to provide code that’s run before and after setting a new value, use willSet and didSet.

For example, the class below ensures that the side length of its triangle is always the same as the side length of its square.

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: "another test shape")
print(triangleAndSquare.square.sideLength)
print(triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)

Optional Values in Class

When working with optional values, you can write ? before operations like methods, properties, and subscripting. If the value before the ? is nil, everything after the ? is ignored and the value of the whole expression is nil.

let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength

Enumerations and Structures

Enum

Use enum to create an enumeration. Like classes and all other named types, enumerations can have methods associated with them.

By default, Swift assigns the raw values starting at zero and incrementing by one each time.

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

Write a function that compares two Rank values by comparing their raw values.

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)
        }
    }
    func isBigger(first: Self, second: Self) -> Bool {
        return first.rawValue > second.rawValue
    }
}

Use the init?(rawValue:) initializer to make an instance of an enumeration from a raw value.

if let convertedRank = Rank(rawValue: 3) {
    let threeDescription = convertedRank.simpleDescription()
}
enum Suit {
    case spades, hearts, diamonds, clubs

    func simpleDescription() -> String {
        switch self {
            case .spades:
                return "spades"
            case .hearts:
                return "hearts"
            case .diamonds:
                return "diamonds"
            case .clubs:
                return "clubs"
        }
    }
}
let hearts = Suit.hearts
let heartsDescription = hearts.simpleDescription()

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

enum Suit {
    case spades, hearts, diamonds, clubs

    func color() -> String {
        switch self {
            case .spades:
                return "black"
            case .hearts:
                return "red"
            case .diamonds:
                return "red"
            case .clubs:
                return "black"
        }
    }

}

or shorter:

enum Suit {
    case spades, hearts, diamonds, clubs

    func color() -> String {
        switch self {
            case .spades, .clubs:
                return "black"
            case .hearts,.diamonds :
                return "red"
        }
    }

}

Struct

Use struct to create a structure.

Differences between struct and class

One of the most important differences between structures and classes is that structures are always copied when they’re passed around in your code, but classes are passed by reference.

You should know these 5 differences between a struct and a class in swift

In a MMVM pattern, we use struct for our data or UI, because the UI might have many Text() but their content can vary. But we usually have only one sources where the UI gets its data, and that is throught the ViewModule, so everyone talking about a VM, they should talk the same VM.

Creating Structs

struct Card{
    var rank: Rank
    var suit: Suit

    func simpleDescription() -> String {
        return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
    }
}

let threeOfSpadex = Card (rank: .three, suit: .spades)
threeOfSpadex.simpleDescription()
"The 3 of spades"

Concurrency

async

func fetchUserID(from server:String) async -> Int {
    if server == "primary" {
        return 97
    }
    return 501
}

await

func fetchUsername(from server:String) async -> String {
    let userID = await fetchUserID(from: server)
    if userID == 501 {
        return "John"
    } 
    return "Guest"
}

async let

Use async let to call an asynchronous function, letting it run in parallel with other asynchronous code. When you use the value it returns, write await.

func connectUser(to server: String) async {
    async let userID = fetchUserID(from: server)
    async let username = fetchUsername(from: server)
    let greeting = await "Hello \(username), user ID \(userID)"
    print(greeting)
}

In the code above, we can fetch the userID and the username in paralell because they are indepent to each other.

Task

Use Task to call asynchronous functions from synchronous code, without waiting for them to return.

Task {
    await connectUser(to: "primary")
}
// Prints "Hello Guest, user ID 97"

Protocols and Extensions

Protocol used to add requirements ot to add standrads to classes, structes or enumerations. Protocols similar to the Interfaces in Java, or somehow a little bit like templates in c++.(not that much)

Declaring a protocol

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

Classes, enumerations, and structures can all adopt protocols.

Class adopting protocol

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()
a.simpleDescription
"A very simple class.  Now 100% adjusted."

Struct adopting protocol

struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}
var b = SimpleStructure()
b.adjust()
b.simpleDescription
"A simple structure (adjusted)"

mutating

Notice the use of the mutating keyword in the declaration of SimpleStructure to mark a method that modifies the structure. The declaration of SimpleClass doesn’t need any of its methods marked as mutating because methods on a class can always modify the class.

extension

Use extension to add functionality to an existing type, such as new methods and computed properties.

extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
 }
print(7.simpleDescription)

Write an extension for the Double type that adds an absoluteValue property.

extension Double: ExampleProtocol {
    var simpleDescription: String {
        return "The double number \(self)"
    }
    mutating func adjust() {
        self += 42.0
    }
    mutating func absoluteValue() {
        if self < 0 {self =  self * -1 }
    }
}

var testDouble = -3.14
testDouble.absoluteValue()
print(testDouble)
3.14

If a class is behaves like a protocol ( a protocol type), class should only access the vars and funcs that are provided from the protocol. you can’t accidentally access methods or properties that the class implements in addition to its protocol conformance.

Error Handling

writing Errors

You represent errors using any type that adopts the Error protocol.

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

` throw`

Use throw to throw an error and throws to mark a function that can throw an error. If you throw an error in a function, the function returns immediately and the code that called the function handles the error.

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }
    return "Job sent"
}

do-catch

Inside the do block, you mark code that can throw an error by writing try in front of it. Inside the catch block, the error is automatically given the name error unless you give it a different name.

do {
    let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
    print(printerResponse)
} catch {
    print(error)
}
Job sent

Change the printer name to “Never Has Toner”, so that the send(job:toPrinter:) function throws an error.

do {
    let printerResponse = try send(job: 1040, toPrinter: "Never Has Toner")
    print(printerResponse)
} catch {
    print(error)
}
noToner

You can provide multiple catch blocks that handle specific errors. You write a pattern after catch just as you do after case in a switch.

do {
    let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
    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)
}

Add code to throw an error inside the do block. What kind of error do you need to throw so that the error is handled by the first catch block? What about the second and third blocks?

1st

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }else if printerName == "Make Fire" {
        throw PrinterError.onFire
    }
    return "Job sent"
}
do {
    let printerResponse = try send(job: 1440, toPrinter: "Make Fire")
    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)
}
I'll just put this over here, with the rest of the fire.

2nd

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }else if printerName == "Make Fire" {
        throw PrinterError.onFire
    }else if printerName == "Other" {
        throw PrinterError.outOfPaper
    }
    return "Job sent"
}
do {
    let printerResponse = try send(job: 1440, toPrinter: "Other")
    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)
}
Printer error: outOfPaper.

3rd

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

enum NetworkError: Error {
    case noConnection
}

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }else if printerName == "Make Fire" {
        throw PrinterError.onFire
    }else if printerName == "Other" {
        throw PrinterError.outOfPaper
    }else if printerName == "Something Else?" {
        throw NetworkError.noConnection
    }
    return "Job sent"
}
do {
    let printerResponse = try send(job: 1440, toPrinter: "Something Else?")
    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)
}
noConnection

try

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

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")
print(printerSuccess)
print(printerFailure)
Optional("Job sent")
nil

defer

Use defer to write a block of code that’s executed after all other code in the function, just before the function returns. The code is executed regardless of whether the function throws an error.

You can use defer to write setup and cleanup code next to each other, even though they need to be executed at different times.

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
}
fridgeContains("banana")
print(fridgeIsOpen)

I think we can use defer for file handling, whatever we do, we still need to close the file after reading or writing anything.

Generics

Write a name inside angle brackets to make a generic function or type.

Generics aka “we don’t care the type”

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
    var result: [Item] = []
    for _ in 0..<numberOfTimes {
         result.append(item)
    }
    return result
}
makeArray(repeating: "knock", numberOfTimes: 4)

We use Generics (also in other languages) to represent something, we do not want to know it’s type or it can be any other basic data type, but we want it to be consistent as a single type during the class scope (or in struct)

As in above code, we can make array of anything, but we can only provide the func some type of something.

You can make generic forms of functions and methods, as well as classes, enumerations, and structures.

// Reimplement the Swift standard library's optional type
enum OptionalValue<Wrapped> {
    case none
    case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)

Use where right before the body to specify a list of requirements—for example, to require the type to implement a protocol, to require two types to be the same, or to require a class to have a particular superclass.

[ I think this is same as assert in Java and Dart, to check the conditions before doing the real initialization or action ].

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
}
anyCommonElements([1, 2, 3], [3])