🌙
☀️ Dark
PART 2

Swift Engineering

Value vs Reference semantics, ARC, memory ownership, and abstractions.

Intermediate 45 min read
The Engineer's Bible — Swift & iOS: Volume 2, Part 2

PART 2 — SWIFT ENGINEERING

Chapter 1: Memory Semantics & Ownership

Learning Objectives

The reader will understand the fundamental differences between value and reference semantics, how copy-on-write optimizes memory, how Automatic Reference Counting (ARC) operates internally, and how to intentionally prevent retain cycles using weak and unowned references.

Prerequisites

Structs, Classes, Optionals, and Initialization.

Why Does This Exist?

Memory must be managed. When an object is created, it takes up RAM. When it is no longer needed, that RAM must be freed. If we free it too early, the application crashes. If we never free it, the application runs out of memory and crashes. We need a system to predictably manage state and memory ownership across complex application graphs.

The Problem Before the Solution

Before ARC and Swift's value semantics, developers had to manually retain and release objects (Manual Retain-Release or MRR). You had to explicitly call retain() when keeping an object and release() when done.

Why the Old Approach Breaks

Manual memory management relies on human perfection. A single missed release() causes a memory leak. A single extra release() causes a dangling pointer and a crash. As applications grew in complexity, tracking ownership mentally became impossible.

History

Apple introduced ARC in 2011 to automate the retain/release calls at compile time. Swift later emphasized value semantics (structs) to avoid shared mutable state entirely, fundamentally changing how engineers think about data.

Mental Model

Imagine a Google Doc vs. a PDF download. A class is a Google Doc (Reference Semantics) — if you share the link, everyone edits the same document. A struct is a downloaded PDF (Value Semantics) — if you email it to someone, they get their own copy. Edits to their PDF do not affect yours.

Now remove the analogy. Here is what Swift actually does: Reference types (classes) store their data on the heap and pass around pointers. Value types (structs) pass the actual data, copying it when assigned or mutated.

Internal Working

When you pass a value type, Swift copies the bytes. For large collections like Arrays, Swift uses Copy-on-Write (CoW). It shares the heap buffer until a mutation occurs, at which point it copies the buffer.

For reference types, Swift uses ARC. Every class instance has a hidden integer called a retain count. When a strong reference points to it, the count increments. When the reference goes out of scope, the count decrements. When it hits zero, Swift immediately deallocates the memory.

Visual Explanation

Reference Semantics:
Var A ----> [ Heap: User Object (Count: 2) ]
Var B ----/

Value Semantics:
Var C ----> [ Stack: User Data ]
Var D ----> [ Stack: Copied User Data ]
    

Syntax

class User { ... } // Reference type
struct Data { ... } // Value type
weak var parent: Node? // Weak reference
unowned let owner: Node // Unowned reference
    

Tiny Example

class ReferenceType { var value = 0 }
struct ValueType { var value = 0 }

var ref1 = ReferenceType()
var ref2 = ref1
ref2.value = 10 // ref1.value is now 10

var val1 = ValueType()
var val2 = val1
val2.value = 10 // val1.value remains 0
    

Walkthrough

In the tiny example, ref1 and ref2 both point to the same memory address. Modifying one modifies the other. val1 and val2 are entirely independent copies. Swift manages the memory for the class using ARC, and for the struct, it manages it automatically on the stack (in most cases).

Break It

Let's create a retain cycle.

class Node {
    var next: Node?
}
let nodeA = Node()
let nodeB = Node()
nodeA.next = nodeB
nodeB.next = nodeA // Retain cycle! Memory leak!
    

Debug It

Use the Xcode Memory Graph Debugger. It will show a strong reference cycle between nodeA and nodeB. To fix it, we must break the cycle using a weak reference, which does not increase the retain count.

class Node {
    weak var next: Node?
}
    

Mini Project

Build a simple cache system that stores heavy image data using value semantics but manages cache expiration using classes.

Real Application Feature

A parent-child view coordinator system where the parent holds strong references to child coordinators, and children hold weak references back to the parent to trigger navigation events.

Production Implementation

In production, you use structs for all domain models (User, Post, Comment). You use classes for stateful services or view models. Parent coordinators own child coordinators strongly, children delegate back via weak protocols.

Production Usage

Swift's standard library extensively uses Copy-on-Write for String, Array, and Dictionary to provide value semantics with reference-level performance.

Performance

Value semantics reduce heap allocations and synchronization overhead. Copy-on-Write ensures that copies are extremely cheap O(1) operations until mutation occurs. However, copying massive structs heavily can thrash the CPU cache. Measure before optimizing.

Best Practices

Default to structs. Use classes when you specifically need shared mutable state, objective-c interoperability, or lifecycle control (deinit).

Interview Questions

Q: What is the difference between weak and unowned?

View Answer

A weak reference allows the object to become nil, so it must be an Optional. An unowned reference assumes the object will never be deallocated before the reference itself, so it does not need to be optional. If you access an unowned reference after the object is deallocated, the app crashes.

Engineering Challenge

Design a tree data structure that allows parent and child traversal without leaking memory.

View Solution

The Tree nodes should be classes. The parent holds an array of strong references to its children. Each child holds a `weak` reference to its parent.

Revision Sheet

Struct = Value = Copy. Class = Reference = Share. ARC = Automated retain counting. Retain Cycle = Two strong references pointing to each other. Weak = No retain count, becomes nil. Unowned = No retain count, crashes if accessed after deallocation.

Connections

Connects to: Concurrency (Actors are reference types), SwiftUI (Views are value types, Observable objects are reference types).

Chapter 2: Protocol-Oriented Programming & Abstraction

Learning Objectives

The reader will understand how to use protocols, generic specialization, and dependency injection to build highly abstract, decoupled, and testable architectures.

Prerequisites

Protocols, Generics, Value Semantics.

Why Does This Exist?

Code needs to be flexible. If a system tightly couples to a specific implementation (e.g., a MySQL database), changing it later (to CoreData) requires rewriting the entire system.

The Problem Before the Solution

In classical Object-Oriented Programming (OOP), developers used deep class inheritance hierarchies to share behavior. A NetworkController might inherit from BaseController.

Why the Old Approach Breaks

Inheritance forces coupling. You get the "gorilla holding a banana and the entire jungle" problem. Swift structs cannot inherit, making OOP impossible for value types.

History

Apple introduced Protocol-Oriented Programming (POP) at WWDC 2015. It shifted the community from inheritance-based designs to composition-based designs using protocol extensions.

Mental Model

Inheritance is an "is-a" relationship (A Dog is an Animal). Protocols are an "acts-as" or "has-a" relationship (A Dog acts as Runnable).

Now remove the analogy. A protocol defines a memory layout and a method dispatch table. Protocol extensions provide default implementations without requiring a shared base class.

Internal Working

When you use protocols dynamically (existential types), Swift uses a mechanism called Protocol Witness Tables (PWT) and an Opaque Value Container, adding overhead. When you use generics, Swift uses Monomorphization (Specialization) at compile time, generating unique code for each type, making it as fast as direct calls.

Visual Explanation

Protocol Witness Table (Dynamic Dispatch):
Caller -> PWT -> Actual Implementation

Generics Specialization (Static Dispatch):
Caller -> Direct Call to Special Implementation
    

Syntax

protocol Fetchable {
    associatedtype DataType
    func fetch() -> DataType
}

struct NetworkFetcher: Fetchable {
    func fetch() -> Data { return Data() }
}
    

Tiny Example

protocol Logger { func log(_ message: String) }
struct ConsoleLogger: Logger {
    func log(_ message: String) { print(message) }
}
    

Walkthrough

By defining Logger as a protocol, any component that needs to log only cares that the object conforms to Logger. It does not care if it's a ConsoleLogger or a FileLogger.

Break It

Tightly coupling a view model to a network layer.

class ViewModel {
    let api = RealNetworkService() // Cannot be mocked!
}
    

Debug It

This code cannot be tested without hitting the real network. We debug this architectural flaw by injecting the dependency via a protocol.

Production Implementation (Dependency Injection & Testability)

protocol NetworkService {
    func execute(request: Request) async throws -> Data
}

class ViewModel {
    let api: NetworkService
    init(api: NetworkService) { self.api = api }
}
    

Now, during testing, we can inject a MockNetworkService.

Production Usage

SwiftUI is built entirely on protocols (View). Identifiable, Hashable, and Equatable are the backbone of modern Swift collections.

Performance

Use generics instead of existential types (any Protocol) when possible. Generics allow the compiler to optimize and inline code, avoiding dynamic dispatch overhead.

Best Practices

Start with concrete types. Only extract a protocol when you need multiple implementations (e.g., Real vs Mock for testing).

Mini Project

Time: 20 minutes.

Goal: Implement a Decoupled Logger.

Create a `Logger` protocol with a `log(message: String)` method. Implement two structs: `ConsoleLogger` and `FileLogger`. Inject the protocol into a `UserService` struct and call the log method when a user is created.

💡 See One Approach (Mini Project)

This is one valid solution — yours may differ.

swift
swift
protocol Logger {
    func log(message: String)
}

struct ConsoleLogger: Logger {
    func log(message: String) {
        print("Console: \(message)")
    }
}

struct FileLogger: Logger {
    func log(message: String) {
        print("File: \(message)")
    }
}

struct UserService {
    let logger: Logger
    
    func createUser() {
        logger.log(message: "User created successfully")
    }
}

Bigger Project

Time: 1 hour.

Goal: Design a Swapable Caching Layer.

Design a caching layer that can be swapped between Memory Cache and Disk Cache without changing the consumer. Create a `Cache` protocol with `get` and `set` methods. Implement `MemoryCache` and `DiskCache` structs. Inject the `Cache` protocol into a repository.

💡 See One Approach (Bigger Project)

This is one valid solution — yours may differ.

swift
swift
protocol Cache {
    func set(_ value: String, forKey key: String)
    func get(forKey key: String) -> String?
}

class MemoryCache: Cache {
    private var storage = [String: String]()
    
    func set(_ value: String, forKey key: String) {
        storage[key] = value
    }
    
    func get(forKey key: String) -> String? {
        return storage[key]
    }
}

class DiskCache: Cache {
    func set(_ value: String, forKey key: String) {
        // Fake disk write
    }
    
    func get(forKey key: String) -> String? {
        // Fake disk read
        return nil
    }
}

class DataRepository {
    var cache: Cache
    
    init(cache: Cache) {
        self.cache = cache
    }
    
    func saveData(_ data: String, key: String) {
        cache.set(data, forKey: key)
    }
}

Interview Questions

Easy: What is the difference between a class and a protocol?

View Answer

A class provides a concrete implementation and supports inheritance, while a protocol acts as a blueprint of methods and properties without implementation.

Medium: What is the difference between some View and any View?

View Answer

some (Opaque Type) resolves to a single concrete type at compile time. It is statically dispatched and highly optimized. any (Existential Type) acts as a box that can hold any type conforming to the protocol dynamically at runtime, incurring performance overhead.

Hard: How does Protocol Witness Table (PWT) work?

View Answer

When using existential types (any Protocol), Swift uses a PWT at runtime to look up the correct method implementation for the concrete type, which introduces dynamic dispatch overhead compared to generic specialization.

Revision Sheet

Protocol = Blueprint. Extension = Default behavior. Generics = Compile-time specialization. Existential = Runtime dynamic dispatch. Dependency Injection = Passing dependencies instead of creating them internally.

Connections

Connects to: SwiftUI (some View), Testing (Mocks), and Architecture (Clean Architecture boundaries).

Mini Project (20-30 min)

Build a small script to detect retain cycles using classes and weak references.

View Solution
swift

// Swift Engineering - Retain Cycle Detector
class Node {
    var value: Int
    weak var next: Node? // weak prevents retain cycle
    init(value: Int) { self.value = value }
    deinit { print("Node \(value) deallocated") }
}
                

Bigger Project (1-2 hours)

Design a generic networking layer using Protocol-Oriented Programming (POP) to decouple your app from specific API implementations.

View Solution
swift

// Generic Network Layer using POP
protocol APIRequest {
    associatedtype Response: Decodable
    var url: URL { get }
}
class APIClient {
    func fetch<T: APIRequest>(_ request: T) async throws -> T.Response {
        let (data, _) = try await URLSession.shared.data(from: request.url)
        return try JSONDecoder().decode(T.Response.self, from: data)
    }
}
                

Interview Questions

1. (Easy) What is the difference between value types and reference types in Swift?
View Answer
Value types (like structs and enums) are copied when assigned or passed around, whereas reference types (like classes) share a single instance in memory. Modifying a value type affects only that copy, but modifying a reference type affects all references to it.
2. (Medium) Explain how Copy-on-Write (COW) works under the hood.
View Answer
COW is an optimization used by Swift's standard library collections (Array, Dictionary). When multiple variables point to the same array, they share the same memory buffer. A copy of the underlying data is only made if one of the variables mutates the array, preserving memory and performance.
3. (Hard) How do protocol extensions enable Protocol-Oriented Programming (POP)?
View Answer
Protocol extensions allow you to provide default implementations for protocol requirements. This enables types to adopt behavior horizontally without inheritance, making it possible to share logic across unrelated structs, enums, and classes while keeping the type system flexible and compositional.