Minimalist Swift 6 Tutorial

I am ready to systematically review the iOS knowledge, so I have this simple Swift 6 Programming study notes.

Part 1: Core Concepts

1. Value vs. Reference Types

Types in Swift are divided into value types (struct, enum) and reference types (class). The fundamental difference lies in how their data is stored and passed.

  • Value Types: Each instance keeps a unique copy of its data. When you pass a value type, it is copied.
  • Reference Types: Instances share a single copy of their data. When you pass a reference type, a reference (or pointer) to the instance is passed.

Copy-on-Write

To optimize performance, many of Swift’s standard library value types (like Array and Dictionary) use a technique called Copy-on-Write. This means a copy is only made when the data needs to be modified; otherwise, multiple instances share the same data storage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import Foundation

// A backing class for demonstration
class BackendQueue<T> {
private var items = [T]()
public init() {}
private init(_ items: [T]) { self.items = items }
public func addItem(item: T) { items.append(item) }
public func getItem() -> T? {
if items.count > 0 { return items.remove(at: 0) }
else { return nil }
}
public func count() -> Int { return items.count }
public func copy() -> BackendQueue<T> { return BackendQueue<T>(items) }
}

// A struct wrapper to implement Copy-on-Write
struct Queue {
private var internalQueue = BackendQueue<Int>()

public mutating func addItem(item: Int) {
checkUniquelyReferencedInternalQueue()
internalQueue.addItem(item: item)
}

public func count() -> Int {
return internalQueue.count()
}

mutating private func checkUniquelyReferencedInternalQueue() {
if !isKnownUniquelyReferenced(&internalQueue) {
internalQueue = internalQueue.copy()
print("Making a copy of internalQueue")
} else {
print("Not making a copy of internalQueue")
}
}
}

print("Start")
var queue1 = Queue()
var queue2 = queue1 // No copy happens here

print("ADD")
queue1.addItem(item: 1) // Modifying queue1 triggers a copy
queue1.addItem(item: 2)
print(queue1.count())
print("Done")

/*
Console Output:
Start
ADD
Making a copy of internalQueue
Not making a copy of internalQueue
2
Done
*/

Noncopyable Types (~Copyable)

Swift 6 introduces noncopyable types to represent unique resources like file handles or network sockets, ensuring they are not accidentally duplicated.

1
2
3
4
struct Person: ~Copyable {
var firstName: String
var lastName: String
}

Two key concepts related to noncopyable types are borrowing and consuming.

  • Borrowing (borrowing): Grants temporary, read-only access to a noncopyable value without transferring ownership. Borrowed values are thread-safe.
  • Consuming (consuming): Transfers ownership of a noncopyable value, and the original variable becomes invalid. A consuming method ends the object’s lifetime upon its return. Global instances cannot be consumed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// The `borrowing` keyword indicates the function temporarily borrows `user` without taking ownership
func sendEmail(_ user: borrowing Person) {
print("Sending Email to \(user.firstName)")
}

// The `consuming` keyword indicates the function consumes `user`, taking ownership
func consumeUser(_ user: consuming Person) {
print("Consuming User \(user.firstName)")
}

func userFunction() {
let user = Person(firstName: "Jon", lastName: "Hoffman")
sendEmail(user) // user is borrowed
consumeUser(user) // user is consumed, and the variable becomes invalid afterward
}

We can also create consuming methods that invalidate the instance once executed.

1
2
3
4
5
6
7
8
9
struct SecrectMessage: ~Copyable {
private var message: String
init(_ message: String) { self.message = message }

// After this consuming method is executed, the instance is destroyed
consuming func read() {
print("\(message)")
}
}

2. Enumerations

Enumerations define a common type for a group of related values.

  • Raw Values: Enum members can be prepopulated with a default value.

    1
    2
    3
    4
    5
    6
    enum Direction: String {
    case North = "N", South = "S", West = "W", East = "E"
    }
    enum Month: Int {
    case January = 1, February, March, April // 2, 3, 4 are inferred automatically
    }
  • Associated Values: Store custom values associated with an enum member.

    1
    2
    3
    4
    enum Product {
    case Book(Double, Int, Int) // price, year, pages
    case Puzzle(Double, Int) // price, pieces
    }
  • Pattern Matching: The switch statement makes it easy to handle different enum cases and extract their associated values.

    1
    2
    3
    4
    5
    6
    7
    let masterSwift = Product.Book(49.99, 2024, 394)
    switch masterSwift {
    case .Book(let price, let year, let pages):
    print("Mastering Swift was published in \(year) for \(price) and has \(pages) pages")
    case .Puzzle(let price, let pieces):
    print("A puzzle with \(pieces) pieces and sells for \(price)")
    }
  • Enum Iteration: By conforming to the CaseIterable protocol, you can iterate over all members of an enumeration.

    1
    2
    3
    4
    5
    6
    7
    enum DaysOfWeek: String, CaseIterable {
    case Monday = "Mon", Tuesday = "Tues", Wednesday = "Wed", Thursday = "Thur", Friday = "Fri", Saturday = "Sat", Sunday = "Sun"
    }

    for day in DaysOfWeek.allCases {
    print("-- \(day.rawValue)")
    }

3. Closures

Closures are self-contained blocks of functionality that can be passed around and used in your code.

  • Basic Syntax:

    1
    2
    3
    4
    let clos1 = { () -> Void in
    print("hello world")
    }
    clos1()
  • Shorthand Syntax: Swift provides several ways to simplify closure syntax.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // $0, $1 represent the first and second parameters
    guests.map { print("hello \($0)") }

    // If the closure is the only argument to a function, the parentheses can be omitted
    testFunction2(num: 5) {
    print("hello from \($0)")
    }

    // Single-expression closures can implicitly return their result
    let clos7 = { (first: Int, second: Int) -> Int in first + second }
    print(clos7(1, 2)) // Prints 3
  • Escaping Closures (@escaping): When a closure is called after the function it was passed to returns, it needs to be marked with the @escaping keyword. This typically happens when the closure is stored for later use or executed in an asynchronous operation.

    1
    2
    3
    4
    var handlers: [LogLevel: [logLevelHandler]] = [:]
    func registerHandler(for level: LogLevel, handler: @escaping logLevelHandler) {
    handlers[level, default: []].append(handler)
    }

4. Error Handling

Swift provides a powerful error handling model that allows you to represent and respond to recoverable errors.

  • Defining Errors: Create an enum that conforms to the Error protocol to represent different kinds of errors.

    1
    2
    3
    4
    5
    6
    enum PlayerNumberError: Error {
    case NumberTooHigh(description: String)
    case NumberTooLow(description: String)
    case NumberAlreadyAssigned
    case NumberDoesNotExist
    }
  • Throwing Errors (throws): Use the throw keyword to throw an error within a function. The function’s signature must be marked with throws.

    1
    2
    3
    4
    5
    6
    mutating func addPlayer(player: BaseballPlayer) throws {
    guard player.number < maxNumber else {
    throw PlayerNumberError.NumberTooHigh(description: "Max number is \(maxNumber)")
    }
    // ... other checks
    }
  • Catching Errors (do-catch): Use a do-catch statement to call a function that can throw an error.

    1
    2
    3
    4
    5
    6
    7
    8
    do {
    let player = try myTeam.getPlayerByNumber(number: 34)
    print("Player is \(player.firstName)")
    } catch PlayerNumberError.NumberDoesNotExist {
    print("No player has that number")
    } catch let error { // Catch all other errors
    print("An error occurred: \(error)")
    }

    You can also match multiple error patterns in a single catch clause:

    1
    2
    3
    catch PlayerNumberError.NumberTooHigh, PlayerNumberError.NumberTooLow {
    print("Number is out of range.")
    }
  • LocalizedError Protocol: Conforming to this protocol can provide richer, localized descriptions for your errors.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    enum PlayerNumberError: Error, LocalizedError {
    // ... cases
    var errorDescription: String? {
    switch self {
    case .NumberAlreadyAssigned:
    return "Player number already assigned"
    // ... other descriptions
    }
    }
    }
  • Defer Statement (defer): The code within a defer block is executed just before the current scope is exited, whether by normal completion or by throwing an error. This is very useful for resource cleanup.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func processFile() {
    let file = openFile()
    defer {
    closeFile(file) // Ensures the file is always closed
    print("File closed.")
    }
    // ... file processing code that might throw an error
    print("File processed.")
    }

5. Memory Management

Swift uses Automatic Reference Counting (ARC) to manage memory. ARC tracks the number of references to class instances. When the reference count for an instance drops to zero, the instance is deallocated, and its memory is freed.

1
2
3
4
5
6
7
8
9
10
class MyClass {
var name: String
init(name: String) { self.name = name; print("Initializing \(name)") }
deinit { print("Releasing \(name)") }
}

var ref1: MyClass? = MyClass(name: "One") // Reference count is 1
var ref2: MyClass? = ref1 // Reference count is 2
ref1 = nil // Reference count is 1
ref2 = nil // Reference count is 0, instance is deallocated

Strong Reference Cycles

If two class instances hold a strong reference to each other, they will never be deallocated, causing a memory leak.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass1_Strong {
var class2: MyClass2_Strong?
// ...
}
class MyClass2_Strong {
var class1: MyClass1_Strong?
// ...
}

var class1: MyClass1_Strong? = MyClass1_Strong(name: "Class1")
var class2: MyClass2_Strong? = MyClass2_Strong(name: "Class2")
class1?.class2 = class2 // class1 holds class2
class2?.class1 = class1 // class2 holds class1 (cycle!)

class1 = nil
class2 = nil // Instances will not be released

Solutions:

  • Weak References (weak): Use when the referenced instance might become nil. A weak reference does not increase the reference count and must be an optional type.

    1
    2
    3
    class MyClass2_Weak {
    weak var class1: MyClass1_Weak? // Weak reference
    }
  • Unowned References (unowned): Use when you are certain the referenced instance will never be nil during the current instance’s lifetime. An unowned reference is not optional, and accessing a deallocated unowned reference will trigger a runtime error.

    1
    2
    3
    class MyClass1_Unowned {
    unowned let class2: MyClass2_Unowned // Unowned reference
    }

Part 2: Protocol-Oriented & Functional Programming

Swift is a multi-paradigm language with strong support for Protocol-Oriented Programming (POP) and Functional Programming.

1. Object-Oriented Programming (OOP)

The three pillars of OOP are Encapsulation, Inheritance, and Polymorphism.

  • Inheritance: Supported by reference types (class). While powerful, complex class hierarchies can increase code complexity and coupling, making modification and maintenance difficult.
  • Dynamic Dispatch: When a method on a class is called, the runtime uses a virtual table (VTable) to look up and call the correct implementation. This provides flexibility but is slightly slower than a direct call.

2. Protocol-Oriented Programming (POP)

POP is a core design philosophy in Swift. It emphasizes defining blueprints using protocols rather than relying on class inheritance.

  • Protocol Definition: A protocol defines a blueprint of methods, properties, and other requirements.
    1
    2
    3
    protocol Nameable {
    var firstName: String { get }
    }
  • Protocol Composition: A type can conform to multiple protocols, combining different functionalities.
    1
    2
    protocol Person: Nameable, Contactable { /* ... */ }
    struct Employee: Person, Occupation { /* ... */ }
  • Protocol Inheritance: Protocols can also inherit from other protocols, aggregating multiple requirements.

Compared to class inheritance, POP offers greater flexibility and modularity, helping to avoid bloated base classes.

3. Protocols and Protocol Extensions

  • Type Checking and Casting: You can use is and as? to check if an instance conforms to a protocol.

    1
    2
    3
    for person in people where person is SwiftProgrammer {
    print("\(person.firstName) is a Swift Programmer")
    }
  • Protocol Extensions: You can extend protocols to provide default implementations for methods. Types conforming to the protocol automatically gain this functionality.

    1
    2
    3
    4
    5
    6
    7
    8
    protocol Dog {
    var name: String { get }
    }
    extension Dog {
    func speak() -> String {
    return "Woof Woof"
    }
    }
  • Any vs any:

    • Any: Can represent a value of any type, including function and optional types.
    • any: Used to modify a protocol, representing an existential type. It allows you to store values of different types that conform to the same protocol in a container and supports dynamic dispatch.
  • Implicitly Opened Existentials: Swift 6 allows the compiler to implicitly open existential types, simplifying operations on protocol arrays.

    1
    2
    3
    4
    5
    6
    7
    protocol Drawable { func draw() }
    // No manual casting needed, protocol methods can be called directly
    func drawAll(_ items: [any Drawable]) {
    for item in items {
    item.draw()
    }
    }

4. Generics

Generic code enables you to write flexible, reusable functions and types that can work with any type.

  • Generic Functions:
    1
    2
    3
    func swapGeneric<T>(a: inout T, b: inout T) {
    let tmp = a; a = b; b = tmp
    }
  • Associated Types (associatedtype): Used in a protocol as a placeholder for a type that is specified only when the protocol is adopted.
    1
    2
    3
    4
    5
    protocol Queue {
    associatedtype QueueType
    mutating func add(item: QueueType)
    mutating func getItem() -> QueueType?
    }
  • Conditional Extensions and Conformance: You can add extensions to a generic type that are only available if the generic parameter meets certain conditions.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Add the sum method only if T is a numeric type
    extension List where T: Numeric {
    func sum() -> T { items.reduce(0, +) }
    }

    // List conforms to Equatable only if T also conforms to Equatable
    extension List: Equatable where T: Equatable {
    static func == (l1: List, l2: List) -> Bool {
    // ... comparison logic
    }
    }

5. Functional Programming

Core principles of functional programming include:

  • Immutability: Prefer constants (let) to avoid direct state modification.
    1
    2
    let numbers = [1, 2, 3, 4, 5]
    let doubled = numbers.map { $0 * 2 } // The `numbers` array itself is not changed
  • Pure Functions: Always produce the same output for the same input and have no side effects.
    1
    2
    3
    func add(_ first: Int, _ second: Int) -> Int {
    return first + second
    }
  • Higher-Order Functions: Functions that take other functions as arguments or return them, such as map, filter, and reduce.
    1
    2
    3
    func performMathOperation(_ first: UInt, _ second: UInt, function: (UInt, UInt) -> UInt) -> UInt {
    return function(first, second)
    }

Advanced Techniques:

  • Function Composition: Combining multiple functions into a new one.
    1
    2
    3
    4
    5
    infix operator >>>
    func >>> <A, B, C>(lhs: @escaping (A) -> B, rhs: @escaping (B) -> C) -> (A) -> C {
    return { rhs(lhs($0)) }
    }
    let addOneToString = addOne >>> toString
  • Currying: Transforming a function that takes multiple arguments into a sequence of functions that each take a single argument.
    1
    2
    3
    4
    5
    func curriedAdd(_ a: Int) -> (Int) -> Int {
    return { a + $0 }
    }
    let addTwo = curriedAdd(2)
    let result = addTwo(3) // result is 5
  • Recursion: A function calling itself to solve a problem.
    1
    2
    3
    4
    func factorial(_ n: Int) -> Int {
    if n <= 1 { return 1 }
    return n * factorial(n - 1)
    }

Part 3: Modern Concurrency

Swift provides a concurrency model ranging from the low-level GCD to the modern async/await structured approach.

1. Grand Central Dispatch (GCD)

GCD is a low-level C API that manages tasks via queues.

  • Concurrency: Multiple tasks starting, running, and completing in the same time period.
  • Parallelism: Multiple tasks running at the exact same moment, which requires a multi-core processor.

Queue Types:

  • Serial Queues: Tasks are executed one at a time in FIFO order. Often used to synchronize access to a shared resource.
  • Concurrent Queues: Tasks start in order but can run concurrently. The system determines the number of concurrent tasks.
  • Main Dispatch Queue: A globally available serial queue that executes tasks on the application’s main thread, typically used for UI updates.

Using Queues:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Create a concurrent queue
let cqueue = DispatchQueue(label: "cqueue.example", attributes: .concurrent)
// Create a serial queue
let squeue = DispatchQueue(label: "squeue.example")

// Execute a task asynchronously without blocking the current thread
cqueue.async {
performCalculation(tag: "async1")
}

// Switch from a background thread to the main thread to update the UI
squeue.async {
let resizedImage = image.resize()
DispatchQueue.main.async {
picView.image = resizedImage
}
}

Advanced Tools:

  • DispatchGroup: Coordinate the completion of multiple asynchronous tasks.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let group = DispatchGroup()
    group.enter()
    queue.async { /* task 1 */; group.leave() }
    group.enter()
    queue.async { /* task 2 */; group.leave() }

    group.notify(queue: .main) {
    print("All tasks are complete")
    }
  • Barrier: Create a synchronization point in a concurrent queue. All tasks submitted before the barrier complete before the barrier task executes. Tasks submitted after the barrier wait for it to finish.
    1
    2
    3
    queue.async(flags: .barrier) {
    // This task waits for previous tasks and blocks subsequent ones
    }
  • DispatchSemaphore: Control the number of concurrent accesses to a shared resource.
    1
    2
    3
    4
    5
    6
    let semaphore = DispatchSemaphore(value: 1) // Allow only one thread to access
    func accessSharedResource() {
    semaphore.wait() // Request access, wait if count is 0
    // ... access shared resource ...
    semaphore.signal() // Finished, release the resource
    }

2. Structured Concurrency (async/await)

Swift 6 emphasizes using structured concurrency to write safer, more readable asynchronous code, preventing issues like data races.

async and await

  • async: Marks a function as asynchronous, meaning it can be suspended during its execution.
  • await: Used to call an async function, indicating that the current task might pause here to wait for the result of the asynchronous function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func retrieveUserData() async -> String {
print("Retrieving user data")
try? await Task.sleep(nanoseconds: 2_000_000_000)
return "User Data Retrieved"
}

// Serial execution
let data = await retrieveUserData()

// Parallel execution
async let userData = retrieveUserData()
async let imageData = retrieveImageData()
let results = await (userData, imageData)
print("User Data: \(results.0), Image Data: \(results.1)")

Tasks

A Task represents a unit of work that can be run asynchronously.

  • Creating a Task:
    1
    2
    3
    4
    Task {
    let data = await retrieveUserData()
    print("Data: \(data)")
    }
  • Detached Task (Task.detached): Creates a top-level task that does not inherit the context (like actor isolation) of its creation point.
  • Task Cancellation: Tasks can be cancelled externally. In a long-running loop, you should periodically check Task.isCancelled and exit gracefully.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func testCancelTask() async throws {
    for i in 0..<10 {
    if Task.isCancelled {
    print("Task was cancelled, cleaning up")
    throw CancellationError()
    }
    print("Loop \(i)")
    await retrieveUserData()
    }
    }

Task Groups

Used for creating a dynamic group of concurrent tasks and waiting for all of them to complete.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func taskGroup() async -> [String] {
return await withTaskGroup(of: String.self) { group in
let users = ["Jon", "Heidi", "Kailey", "Kai"]
for user in users {
group.addTask {
return await retrieveUserData(user)
}
}

var data = [String]()
for await result in group {
data.append(result)
}
return data
}
}

Actors

Actors are a special kind of reference type that protect their mutable state from concurrent access, preventing data races. Access to an actor’s internal state is asynchronous and serialized.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
actor BankAccount {
private var balance: Double
init(_ balance: Double) { self.balance = balance }

func deposit(amount: Double) {
balance += amount
}

func getBalance() -> Double {
return balance
}
}

let account = BankAccount(5000)
await account.deposit(amount: 100)
let newBalance = await account.getBalance()
print("New Balance: \(newBalance)")

Sendable Types

The Sendable protocol marks types whose values can be safely passed between concurrency domains (e.g., from one actor to another).

  • Automatic Conformance: Swift’s core value types (Int, String, etc.), structs and enums containing only Sendable values, and actor types automatically conform to Sendable.
  • Manual Conformance: A class can conform to Sendable if it is final, all its properties are immutable constants (let), and the types of those properties also conform to Sendable.

Part 4: Advanced & Specialized Features

1. Property Observers and Wrappers

  • Property Observers (willSet/didSet): Execute code before (willSet) or after (didSet) a property’s value is set.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct MyStruct {
    var myProperty: String {
    willSet(newName) {
    print("Preparing to change from \(myProperty) to \(newName)")
    }
    didSet {
    if oldValue != myProperty {
    print("Value changed from \(oldValue) to \(myProperty)")
    }
    }
    }
    }
  • Property Wrappers (@propertyWrapper): Encapsulate the storage and logic of a property into a separate type to reduce code duplication.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @propertyWrapper
    struct MyPropertyWrapper<T> {
    private var value: T
    var wrappedValue: T {
    get { /* return the value */ }
    set { /* modify the value */ }
    }
    init(wrappedValue initialValue: T) {
    self.value = initialValue
    }
    }

    struct MyExample {
    @MyPropertyWrapper var number: Int
    }

2. Key Paths and Dynamic Member Lookup

  • Key Paths: Provide a type-safe way to reference a property of a type. The syntax is \TypeName.propertyName.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct BasketballTeam {
    var city: String
    }
    let cityKeyPath = \BasketballTeam.city
    var team = BasketballTeam(city: "Boston")
    let teamCity = team[keyPath: cityKeyPath] // "Boston"

    // Simpler syntax in map/filter
    let names = people.map(\.name)
    let adults = people.filter { $0.age > 17 } // Traditional way
    let adultsWithKeyPath = people.filter { $0[keyPath: \.age] > 17 }
  • Dynamic Member Lookup (@dynamicMemberLookup): Allows a type to access members dynamically using dot syntax, even if those members are not explicitly defined at compile time.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @dynamicMemberLookup
    struct BaseballTeam {
    let city: String
    let nickName: String

    subscript(dynamicMember key: String) -> String {
    switch key {
    case "fullname":
    return "\(city) \(nickName)"
    default:
    return "Unknown"
    }
    }
    }
    let team = BaseballTeam(city: "Boston", nickName: "Red Sox")
    print(team.fullname) // Prints "Boston Red Sox"

3. Custom Subscripting

Allows you to access instances of a type by index, similar to an array or dictionary.

  • Basic Subscript:
    1
    2
    3
    4
    5
    6
    7
    class MyNames {
    private var names = ["Jon", "Kailey", "Kai"]
    subscript(index: Int) -> String {
    get { return names[index] }
    set { names[index] = newValue }
    }
    }
  • Multi-dimensional Subscript:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct TicTacToe {
    var board = [["","",""],["","",""],["","",""]]
    subscript(x: Int, y: Int) -> String {
    get { return board[x][y] }
    set { board[x][y] = newValue }
    }
    }
    var board = TicTacToe()
    board[1, 1] = "x"
  • Static Subscript:
    1
    2
    3
    4
    5
    6
    struct Hello {
    static subscript(name: String) -> String {
    return "Hello \(name)"
    }
    }
    let greeting = Hello["Jon"]

4. Result Builders

Result builders are a special syntax transformation that lets you build a complex result from a sequence of statements, commonly used for creating Domain-Specific Languages (DSLs), like SwiftUI’s view builder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@resultBuilder
struct StringBuilder {
static func buildBlock(_ components: String...) -> String {
return components.joined(separator: " ")
}
}

func buildString(@StringBuilder _ components: () -> String) -> String {
return components()
}

let result = buildString {
"Hello,"
"Mastering"
"Swift!"
}
print(result) // "Hello, Mastering Swift!"

5. Reflection

Swift’s Mirror API allows you to inspect the properties, types, and values of an instance at runtime. Swift’s reflection is read-only, in keeping with its principle of type safety.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let person = Person(firstName: "Jon", lastName: "Hoffman", age: 55)
let mirror = Mirror(reflecting: person)

for (label, value) in mirror.children {
print("Property: \(label ?? "Unknown"), Value: \(value)")
}

// Can be used to implement a generic serialization function
func serialize<T>(_ value: T) -> [String: Any] {
let mirror = Mirror(reflecting: value)
var result = [String: Any]()
for child in mirror.children {
if let propertyName = child.label {
result[propertyName] = child.value
}
}
return result
}

6. Regular Expressions

Swift provides modern and powerful support for regular expressions.

  • Literal Syntax:
    1
    2
    3
    4
    5
    6
    let pattern = /\b\w+\b/
    let text = "Hello from regex literal"
    let matches = text.matches(of: pattern)
    for match in matches {
    print("-- \(text[match.range])")
    }
  • RegexBuilder: Construct complex regular expressions in a declarative way.
    1
    2
    3
    4
    5
    6
    7
    8
    import RegexBuilder

    let pattern = Regex {
    Anchor.wordBoundary
    OneOrMore(.word)
    "@"
    // ... more components
    }
  • Capturing: You can define references to capture matched parts and perform type conversions.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let animalTypeRef = Reference(Substring.self)
    let ageRef = Reference(Int.self)

    let pattern = Regex {
    "I am a "
    Capture(as: animalTypeRef) { OneOrMore(.word) }
    " who is "
    TryCapture(as: ageRef) { OneOrMore(.digit) } transform: { Int($0) }
    // ...
    }

    if let match = str.firstMatch(of: pattern) {
    print("Animal Type: \(match[animalTypeRef])")
    print("Age: \(match[ageRef])")
    }

Part 5: Code Quality & Organization

1. Access Control

Access control restricts access to parts of your code from code in other source files and modules.

  • open: The most permissive. Allows access and subclassing from any module (for classes only).
  • public: Allows access from any module.
  • internal: The default level. Allows access only within the defining module.
  • fileprivate: Allows access only within the defining source file.
  • private: The most restrictive. Allows access only within the enclosing declaration (like a struct or class).

Best Practices:

  • Default to the most restrictive access level and relax it as needed.
  • Encapsulate implementation details and expose only the necessary API.
  • Maintain consistency by following a uniform access control strategy throughout your codebase.

2. Availability Checks

Use #available to check the operating system version and execute different code accordingly.

1
2
3
4
5
if #available(iOS 16.0, macOS 13.0, *) {
// Use new APIs for iOS 16+ and macOS 13+
} else {
// Provide a fallback for older versions
}

You can also mark an entire function or type with @available.

1
2
3
4
@available(iOS 16.0, *)
func newFeature() {
// This function is only available on iOS 16 and later
}

3. Swift Testing

Swift provides the Testing framework, a modern and expressive solution for testing.

  • @Test: Marks a function as a test function.

  • #expect and #require:

    • #expect: Checks if a condition is true. If it fails, the test continues to run.
    • #require: Checks if a condition is true. If it fails, the test terminates immediately.
    1
    2
    3
    4
    5
    @Test func validExpectation() throws {
    #expect(1 == 1)
    let one: Int? = 10
    let willSucceed = try #require(one) // Succeeds, test continues
    }
  • Test Suites (@Suite): Used to organize related tests. Any struct containing @Test functions automatically becomes a suite.

    1
    2
    3
    4
    @Suite("Calculator Tests")
    struct CalculatorTests {
    @Test func testAddition() { /* ... */ }
    }
  • Parameterized Tests: Provide multiple sets of inputs and expected outputs for a single test.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct TestValues { let first: Double, second: Double, answer: Double }

    @Test("Addition Tests", arguments: [
    TestValues(first: 2, second: 3, answer: 5),
    TestValues(first: 10, second: 11, answer: 21)
    ])
    func testAddition(_ values: TestValues) {
    #expect(Calculator.addition(values.first, values.second) == values.answer)
    }
  • @testable: Imports a module and allows test code to access its internal members.

    1
    @testable import MyApp