Swift Engineering
Value vs Reference semantics, ARC, memory ownership, and abstractions.
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.
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.
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 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
// 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)
}
}