Skip to content

Swift Cheatsheet

Apple's language for iOS, macOS, and beyond.

01

Basics

Variables & Constants

Use let by default for values that don't change; switch to var only when mutation is needed. Swift infers types at compile time, but explicit annotations improve readability for complex or ambiguous types.

swift
var name = "Alice"        // mutable
let age = 30               // immutable constant
let pi: Double = 3.14159   // explicit type
var count: Int = 0
count = 10
print(type(of: age))       // Int

Optionals

Optionals represent the absence of a value. Use if-let for safe unwrapping, ?? for default values, and ! only when you're certain the value exists (risky—can crash).

swift
var nickname: String? = nil
nickname = "Al"
if let n = nickname {
    print("Hi, \(n)")
}
print(nickname ?? "Anonymous")  // nil-coalescing
let force: String = nickname!   // force unwrap (crashes if nil)

Tuples

Tuples group multiple values into a single compound value. Useful for returning multiple values from functions. Named elements improve readability.

swift
let person = (name: "Alice", age: 30)
print(person.name)   // Alice
print(person.0)      // Alice
let (n, a) = person
print("\(n), \(a)")  // Alice, 30

Type Conversion

Swift does not implicitly convert between types. Always use explicit conversion (e.g., Double(intVal)) to avoid ambiguity and prevent subtle bugs.

swift
let intVal = 42
let doubleVal = Double(intVal)
let strVal = String(intVal)
let fromStr = Int("100")!   // 100
let invalid = Int("abc")    // nil
print(doubleVal, strVal)

Assertions & Preconditions

Use assert for debugging checks (removed in release builds) and precondition for critical invariants checked in all builds. Both help catch logic errors early.

swift
let age = -5
assert(age >= 0, "Age cannot be negative")
precondition(age >= 0, "Age must be non-negative")
// In debug builds, assert crashes if false
// precondition checks in release builds too
02

Strings & Characters

String Interpolation

String interpolation with \(expr) embeds any expression into a string. It's type-safe and evaluated at compile time, making it safer than format strings.

swift
let name = "Alice"
let age = 30
let msg = "Name: \(name), Age: \(age)"
let calc = "5 + 3 = \(5 + 3)"
print(msg)        // Name: Alice, Age: 30
print(calc)       // 5 + 3 = 8

Common String Methods

Swift strings are Unicode-correct, meaning count reflects actual characters (grapheme clusters), not bytes. Use these methods instead of manual indexing for safety.

swift
let s = "Hello, World"
print(s.count)              // 12
print(s.uppercased())       // HELLO, WORLD
print(s.lowercased())       // hello, world
print(s.hasPrefix("Hello")) // true
print(s.contains("World"))  // true
print(s.reversed())         // "dlroW ,olleH"

Substring & Indexing

String indices are not integers because characters can have different byte sizes. Use index(_:offsetBy:) for navigation. Substrings share memory with the original—convert to String for long-term storage.

swift
let s = "Hello, World"
let idx = s.index(s.startIndex, offsetBy: 7)
print(s[idx])                     // W
let sub = s[idx...]               // "World"
let newStr = String(sub)          // convert to String
print(s.prefix(5))                // "Hello"

Multiline Strings

Triple-quoted strings preserve line breaks and indentation. The closing """ determines the baseline indentation. Ideal for HTML, JSON, or long text blocks.

swift
let poem = """
Roses are red,
Violets are blue,
Swift is great,
And so are you.
"""
print(poem)
// Use \\(expr) for interpolation in multiline

String Building & Splitting

Use joined(separator:) to concatenate with a delimiter and split(separator:) to tokenize. These are more efficient than manual loops with + concatenation.

swift
let parts = ["apple", "banana", "cherry"]
let joined = parts.joined(separator: ", ")
print(joined)  // apple, banana, cherry
let csv = "a,b,c"
let fields = csv.split(separator: ",")
print(fields)  // ["a", "b", "c"]
let reversed = String(s.reversed())
03

Data Structures

Array

Arrays are ordered, zero-indexed collections. Use append/insert for additions, filter/map/reduce for transformations. Prefer value types (Array is a struct) for thread safety.

swift
var nums = [1, 2, 3]
nums.append(4)
nums.insert(0, at: 0)
nums[1] = 20
print(nums.count)        // 5
print(nums.contains(20)) // true
let evens = nums.filter { $0 % 2 == 0 }
print(nums.first, nums.last)

Dictionary

Dictionaries store key-value pairs with O(1) average lookup. Use default: to avoid nil when accessing missing keys. Keys must be Hashable.

swift
var ages: [String: Int] = ["Alice": 30]
ages["Bob"] = 25
ages["Alice"] = 31
print(ages["Alice", default: 0])  // 31
for (name, age) in ages {
    print("\(name): \(age)")
}
print(ages.keys.sorted())

Set

Sets store unique values with O(1) membership testing. Ideal for deduplication and set operations (union, intersection, difference). Elements must be Hashable.

swift
var a: Set<Int> = [1, 2, 3]
var b: Set<Int> = [3, 4, 5]
print(a.union(b))           // {1,2,3,4,5}
print(a.intersection(b))    // {3}
print(a.subtracting(b))     // {1,2}
print(a.isSubset(of: b))    // false
a.insert(6)

Ranges

..< is a half-open range (excludes upper bound), ... is a closed range (includes both). Ranges are useful in loops, slicing, and pattern matching with the ~= operator.

swift
for i in 0..<5 { print(i) }   // 0,1,2,3,4
for i in 0...5 { print(i) }   // 0,1,2,3,4,5
let nums = Array(1...5)       // [1,2,3,4,5]
let prefix = nums.prefix(3)   // [1,2,3]
if 5...10 ~= 7 { print("in range") }

Higher-Order Functions

map transforms each element, filter selects matching elements, reduce combines all into one. These are the foundation of functional programming in Swift and enable concise, readable data pipelines.

swift
let nums = [1, 2, 3, 4, 5]
let doubled = nums.map { $0 * 2 }       // [2,4,6,8,10]
let evens = nums.filter { $0 % 2 == 0 } // [2,4]
let sum = nums.reduce(0, +)             // 15
let strs = nums.map { String($0) }      // ["1","2",...]
print(doubled, evens, sum)
04

Control Flow

If / Else If / Else

Standard conditional branching. Conditions must be Boolean (Bool type). Swift requires braces even for single statements to prevent bugs like Apple's goto fail vulnerability.

swift
let score = 85
if score >= 90 {
    print("A")
} else if score >= 80 {
    print("B")
} else if score >= 70 {
    print("C")
} else {
    print("F")
}

Switch (Pattern Matching)

Swift switch is powerful: supports value binding, tuples, where guards, and ranges. It must be exhaustive (default covers remaining cases) and does not fall through by default.

swift
let point = (2, 0)
switch point {
case (0, 0):
    print("origin")
case (_, 0):
    print("on x-axis")
case (0, _):
    print("on y-axis")
case let (x, y) where x == y:
    print("on diagonal")
default:
    print("elsewhere: \(point.0),\(point.1)")
}

For-In Loops

For-in iterates over ranges, arrays, dictionaries, and any Sequence. Use enumerated() when you need the index. Use _ to ignore loop variables you don't use.

swift
for i in 0..<5 { print(i) }
let fruits = ["apple", "banana"]
for (i, fruit) in fruits.enumerated() {
    print("\(i): \(fruit)")
}
let dict = ["a": 1, "b": 2]
for (k, v) in dict { print("\(k)=\(v)") }
for _ in 0..<3 { print("tick") }

While & Repeat-While

while checks the condition before each iteration; repeat-while checks after (like do-while in C). Use repeat-while when the body must execute at least once.

swift
var n = 5
while n > 0 {
    print(n)
    n -= 1
}
var x = 0
repeat {
    x += 1
} while x < 3
print(x)  // 3

Guard (Early Exit)

guard provides early exit when prerequisites aren't met. Unwrapped optionals remain available in the rest of the scope, reducing nesting. Prefer guard over deep if-let nesting.

swift
func greet(_ name: String?) {
    guard let n = name, !n.isEmpty else {
        print("No name provided")
        return
    }
    print("Hello, \(n)")
    // n is unwrapped and available here
}
greet("Alice")
greet(nil)
05

Functions & Closures

Function Definition

Use _ to omit argument labels for readability. Default parameter values make parameters optional. Functions with a single expression have implicit return (Swift 5.9+).

swift
func greet(_ name: String, greeting: String = "Hello") -> String {
    return "\(greeting), \(name)!"
}
print(greet("Alice"))               // Hello, Alice!
print(greet("Bob", greeting: "Hi")) // Hi, Bob!
func add(_ a: Int, _ b: Int) -> Int { a + b }
print(add(3, 4))  // 7

Multiple Return Values

Tuples enable returning multiple named values. Returning an optional tuple signals possible failure. Access via r.min or r.0—named elements are clearer.

swift
func minMax(_ nums: [Int]) -> (min: Int, max: Int)? {
    guard let first = nums.first else { return nil }
    var mn = first, mx = first
    for n in nums { mn = min(mn, n); mx = max(mx, n) }
    return (mn, mx)
}
if let r = minMax([3, 1, 4, 1, 5]) {
    print("min=\(r.min), max=\(r.max)")
}

Closures

Closures are self-contained blocks of functionality. Use $0, $1 for shorthand argument names. Trailing closure syntax (omitting the last argument label) is idiomatic for map/filter/reduce.

swift
let square: (Int) -> Int = { x in x * x }
print(square(5))  // 25
let add: (Int, Int) -> Int = { $0 + $1 }
print(add(3, 4))  // 7
let nums = [1, 2, 3]
let doubled = nums.map { $0 * 2 }
print(doubled)  // [2, 4, 6]

Escaping & Auto-Closures

@escaping marks closures stored or called after the function returns (needed for async callbacks). @autoclosure wraps an expression in a closure, delaying its evaluation (used in assert).

swift
var handlers: [() -> Void] = []
func saveHandler(_ fn: @escaping () -> Void) {
    handlers.append(fn)
}
saveHandler { print("done") }
handlers.first?()
// @autoclosure delays evaluation
func logIfTrue(_ cond: @autoclosure () -> Bool) {
    if cond() { print("true") }
}
logIfTrue(2 > 1)

Inout Parameters

inout parameters allow functions to modify the caller's variable (pass by reference). Use sparingly—prefer returning new values for clarity. The & prefix marks the mutation site.

swift
func swap(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}
var x = 10, y = 20
swap(&x, &y)
print(x, y)  // 20 10
06

Classes & Structs

Struct (Value Type)

Structs are value types—copied on assignment. Use for simple data containers. Computed properties (distance) calculate on access. mutating methods modify self.

swift
struct Point {
    var x: Double
    var y: Double
    var distance: Double {
        (x * x + y * y).squareRoot()
    }
    mutating func moveBy(dx: Double, dy: Double) {
        x += dx; y += dy
    }
}
var p = Point(x: 3, y: 4)
print(p.distance)  // 5.0

Class (Reference Type)

Classes are reference types—shared via references, support inheritance and deinit. Use when you need identity, shared mutable state, or Objective-C interoperability. Otherwise prefer structs.

swift
class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    deinit { print("\(name) deallocated") }
}
let p = Person(name: "Alice", age: 30)
print(p.name)  // Alice

Inheritance & Override

Use override to redefine a superclass method. Swift uses dynamic dispatch so the subclass version is called. Mark methods final to prevent further overriding for performance.

swift
class Animal {
    func speak() { print("...") }
}
class Dog: Animal {
    override func speak() { print("Woof!") }
}
class Cat: Animal {
    override func speak() { print("Meow!") }
}
let pets: [Animal] = [Dog(), Cat()]
for pet in pets { pet.speak() }

Properties (Computed & Lazy)

Computed properties have get/set blocks; set uses newValue by default. lazy defers initialization until first access—useful for expensive or rarely-needed properties. Must be var.

swift
class Circle {
    var radius: Double
    var area: Double {
        get { .pi * radius * radius }
        set { radius = (newValue / .pi).squareRoot() }
    }
    lazy var heavy = loadExpensiveData()
    init(radius: Double) { self.radius = radius }
}
func loadExpensiveData() -> String { "data" }

Property Observers

willSet/didSet observe property changes. Use didSet to validate or react to changes (e.g., clamping values, triggering UI updates). Observers don't fire during init.

swift
class Counter {
    var count: Int = 0 {
        willSet { print("about to set \(newValue)") }
        didSet {
            print("was \(oldValue), now \(count)")
            if count > 10 { count = 10 }
        }
    }
}
let c = Counter()
c.count = 5
c.count = 20  // clamped to 10
07

Protocols & Extensions

Protocol Definition

Protocols define a blueprint of methods and properties. Types conform by implementing them. Use for abstraction, polymorphism, and decoupling—similar to interfaces in Java/C#.

swift
protocol Greetable {
    var name: String { get }
    func greet() -> String
}
struct User: Greetable {
    let name: String
    func greet() -> String { "Hi, \(name)" }
}
let u = User(name: "Alice")
print(u.greet())  // Hi, Alice

Protocol Extensions (Default Impl)

Protocol extensions provide default implementations. This enables retroactive modeling and code reuse without a base class. A powerful feature for sharing behavior across unrelated types.

swift
protocol Describable {
    func describe() -> String
}
extension Describable {
    func describe() -> String { "A \(type(of: self))" }
}
struct Box: Describable { }
let b = Box()
print(b.describe())  // A Box

Extensions

Extensions add functionality to existing types (even ones you don't own, like Int). Use to organize code, add computed properties, or conform to protocols. Cannot add stored properties.

swift
extension Int {
    var squared: Int { self * self }
    func times(_ block: () -> Void) {
        for _ in 0..<self { block() }
    }
}
print(5.squared)  // 25
3.times { print("hi") }  // prints hi 3 times

Generics

Generics write flexible, reusable code that works with any type while preserving type safety. Use <T> for type parameters. Constraints (where T: Equatable) restrict allowed types.

swift
func stackOf<T>(_ items: T...) -> [T] {
    var arr: [T] = []
    for item in items { arr.append(item) }
    return arr
}
struct Stack<Element> {
    private var items: [Element] = []
    mutating func push(_ e: Element) { items.append(e) }
}
let s = stackOf(1, 2, 3)  // [Int]

Protocol with Associated Type

Associated types let protocols use placeholder types, like generics for protocols. The conforming type specifies the actual type. Use typealias to make it explicit, or let Swift infer it.

swift
protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
    typealias Item = Int
    private var items: [Int] = []
    var count: Int { items.count }
    mutating func append(_ item: Int) { items.append(item) }
    subscript(i: Int) -> Int { items[i] }
}
08

Error Handling

Defining & Throwing Errors

Errors conform to the Error protocol (usually enums). throw signals an error. Associated values (coinsNeeded) carry context. Use throws to mark functions that can fail.

swift
enum VendingError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}
func vend(item: String, coins: Int) throws -> String {
    guard item == "Candy" else { throw VendingError.invalidSelection }
    guard coins >= 2 else { throw VendingError.insufficientFunds(coinsNeeded: 2) }
    return "Dispensing \(item)"
}

Do-Catch & Try

do-catch handles thrown errors. try marks throwing calls. Pattern-match on specific cases for targeted handling. A catch-all handles unexpected errors. Errors propagate up the call stack.

swift
do {
    let result = try vend(item: "Candy", coins: 1)
    print(result)
} catch VendingError.insufficientFunds(let needed) {
    print("Need \(needed) more coins")
} catch VendingError.outOfStock {
    print("Sold out")
} catch {
    print("Other error: \(error)")
}

Try? / Try!

try? converts errors to nil (returns Optional). try! assumes success and crashes on error—use only when failure is truly impossible. Prefer try? with optional binding for graceful degradation.

swift
let result1 = try? vend(item: "Candy", coins: 1)
print(result1)  // nil (error converted to optional)
let result2 = try! vend(item: "Candy", coins: 5)
print(result2)  // Dispensing Candy (crashes if error)
if let r = try? vend(item: "Candy", coins: 5) {
    print(r)
}

Result Type

Result encodes success or failure as a value, enabling async error handling without throw. Use for callbacks, async APIs, or when you want to store/chain outcomes. .get() throws on failure.

swift
enum FetchError: Error { case network, parse }
func fetch(_ url: String) -> Result<String, FetchError> {
    url.isEmpty ? .failure(.network) : .success("data from \(url)")
}
switch fetch("https://api.com") {
case .success(let data): print(data)
case .failure(let err): print("Error: \(err)")
}
let value = try? fetch("").get()

Defer (Cleanup)

defer schedules cleanup code to run when the scope exits, regardless of how (normal return, throw, error). Use for resource release (files, locks). Multiple defers run in LIFO order.

swift
func processFile(_ path: String) throws {
    let handle = openFile(path)
    defer { closeFile(handle) }
    // ... work that might throw ...
    let data = try read(handle)
    // closeFile always runs, even if throw
    print("processed \(data)")
}
09

File I/O & Date/Time

Read & Write Files

Foundation provides simple file APIs. write(toFile:atomically:) writes safely (temp file + rename). Use String(contentsOfFile:) for text. For large files, use FileHandle for streaming.

swift
import Foundation
let path = "test.txt"
let content = "Hello, File I/O!"
try content.write(toFile: path, atomically: true,
                  encoding: .utf8)
let read = try String(contentsOfFile: path, encoding: .utf8)
print(read)  // Hello, File I/O!
let lines = read.split(separator: "\n")
print(lines.count)

URL & FileManager

FileManager handles filesystem operations (create, delete, check existence). Use URLs (not paths) for modern APIs. .documentDirectory is the app's persistent storage on iOS/macOS.

swift
import Foundation
let fm = FileManager.default
let url = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
    .appendingPathComponent("data.json")
try "data".write(to: url, atomically: true, encoding: .utf8)
let exists = fm.fileExists(atPath: url.path)
print("Exists: \(exists)")
try fm.removeItem(at: url)  // delete

Date & DateFormatter

Date represents a point in time (UTC internally). DateFormatter converts between Date and String—always set locale to en_US_POSIX for fixed-format parsing to avoid locale bugs.

swift
import Foundation
let now = Date()
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
fmt.locale = Locale(identifier: "en_US_POSIX")
let str = fmt.string(from: now)
print(str)  // e.g. 2024-01-15 14:30:00
let parsed = fmt.date(from: "2024-01-01 00:00:00")
print(parsed ?? "invalid")

JSON Encoding/Decoding

Codable automates JSON serialization. JSONEncoder/JSONDecoder handle conversion. Conform your types to Codable—the compiler synthesizes the logic. Use CodingKeys to customize key names.

swift
struct User: Codable {
    let name: String
    let age: Int
}
let user = User(name: "Alice", age: 30)
let json = try JSONEncoder().encode(user)
let str = String(data: json, encoding: .utf8)!
print(str)  // {"name":"Alice","age":30}
let decoded = try JSONDecoder().decode(User.self, from: json)
print(decoded.name)  // Alice

Date Calculations

Calendar handles date arithmetic respecting time zones and DST. Use dateComponents to extract fields or compute differences. Never use raw seconds for date math—use Calendar APIs.

swift
import Foundation
let cal = Calendar.current
let now = Date()
let tomorrow = cal.date(byAdding: .day, value: 1, to: now)!
let components = cal.dateComponents([.year, .month, .day], from: now)
print(components.year!, components.month!)
let diff = cal.dateComponents([.hour], from: now, to: tomorrow)
print(diff.hour!)  // 24
10

Concurrency & Async

Async / Await

async/await (Swift 5.5+) makes async code read like sync code. Task creates a new async context. async let runs concurrently and awaits all. Eliminates callback hell—prefer over completion handlers.

swift
func fetchUser(_ id: Int) async -> String {
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    return "User \(id)"
}
Task {
    let user = await fetchUser(42)
    print(user)  // User 42
    async let u1 = fetchUser(1)
    async let u2 = fetchUser(2)
    let users = await [u1, u2]  // concurrent
    print(users)
}

Task & Cancellation

Task represents a unit of async work. Cooperative cancellation via Task.isCancelled—long-running tasks should check periodically. task.value awaits the result. Cancellation is cooperative, not forced.

swift
let task = Task {
    for i in 1...100 {
        if Task.isCancelled { return }
        try? await Task.sleep(nanoseconds: 100_000_000)
        print(i)
    }
}
Task {
    try? await Task.sleep(nanoseconds: 500_000_000)
    task.cancel()  // request cancellation
}
await task.value  // wait for completion

Actors (Thread Safety)

Actors (Swift 5.5+) protect mutable state from data races by serializing access. All access goes through await. Use instead of locks/queues for shared state. The compiler verifies safety.

swift
actor Counter {
    private var count = 0
    func increment() { count += 1 }
    func value() -> Int { count }
}
let counter = Counter()
Task {
    await counter.increment()
    print(await counter.value())
}
// Actors serialize access—no data races

GCD (Grand Central Dispatch)

GCD is the traditional concurrency API. DispatchQueue.global() for background work, .main for UI updates. QoS (.userInitiated, .background) prioritizes tasks. Still useful for non-async code.

swift
import Dispatch
DispatchQueue.global(qos: .userInitiated).async {
    let result = heavyComputation()
    DispatchQueue.main.async {
        print("UI update: \(result)")
    }
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    print("delayed by 2s")
}

Async Sequence

AsyncSequence enables iterating over async values (like a stream). Use for await to consume. Ideal for paginated APIs, server-sent events, or any source producing values over time.

swift
let urls = ["url1", "url2", "url3"]
func fetchAll() async {
    for url in urls {
        let data = await fetchData(url)
        print(data)
    }
}
func fetchData(_ url: String) async -> String {
    "data from \(url)"
}
// AsyncSequence: for await item in stream { ... }
11

Protocols Deep Dive

Protocol with Associated Types

Associated types (associatedtype) let protocols declare a placeholder type that conforming types specify. This is Swift's equivalent of generic type parameters for protocols. The type is inferred from the conforming type's methods. Use 'where' clauses to constrain associated types. PATs (Protocols with Associated Types) can't be used as existential types directly without type erasure or (Swift 5.7+) 'any Container'.

swift
protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

struct IntStack: Container {
    // associatedtype Item inferred as Int
    private var items: [Int] = []
    var count: Int { items.count }
    mutating func append(_ item: Int) { items.append(item) }
    subscript(i: Int) -> Int { items[i] }
}

// Generic constraint with associated type
func sum<C: Container>(_ c: C) -> Int where C.Item == Int {
    var total = 0
    for i in 0..<c.count { total += c[i] }
    return total
}

Protocol Extensions (Default Implementations)

Protocol extensions provide default implementations—conforming types get the method for free but can override it. This is Swift's way to add functionality to types retroactively. Constrained extensions (where Element: Numeric) add methods only to types meeting the constraint. This is how the standard library adds map/filter/reduce to all Collections.

swift
protocol Describable {
    var description: String { get }
}

// Default implementation via extension
extension Describable {
    var description: String { "A \(type(of: self))" }
}

struct Point: Describable {
    let x, y: Int
    // Uses default description: "A Point"
}

struct Person: Describable {
    let name: String
    var description: String { "Person named (name)" }  // override
}

// Extension with constraints
extension Collection where Element: Numeric {
    var sum: Element { reduce(0, +) }
}
[1, 2, 3].sum  // 6

Protocol Composition and Existentials

Protocol composition (A & B) requires a value to conform to multiple protocols. Existential types (any Protocol) can hold any conforming type but have runtime dispatch overhead—a witness table lookup per call. Use 'some Protocol' (opaque return type) when you return one specific type but want to hide it. Prefer generics over existentials for performance; use existentials when you need heterogeneous collections.

swift
protocol Named { var name: String { get } }
protocol Aged { var age: Int { get } }

struct Person: Named, Aged {
    let name: String
    let age: Int
}

// Protocol composition: requires both protocols
func wishHappyBirthday(to celebrant: Named & Aged) {
    print("Happy birthday, (celebrant.name), you're (celebrant.age)!")
}

let p = Person(name: "Alice", age: 30)
wishHappyBirthday(to: p)

// Existential types (any) — type erasure overhead
let items: [any Named] = [p, Pet(name: "Rex")]
for item in items { print(item.name) }

// Swift 5.7: 'any' keyword required for existentials
// 'some' for opaque types (returns one concrete type)

Protocol-Oriented Programming

Protocol-Oriented Programming is Swift's paradigm shift from OOP. Instead of class hierarchies, design around protocols with default implementations. Benefits: works with value types (structs/enums), supports retroactive conformance (extend types you don't own), enables multiple 'inheritance' (a type can conform to many protocols). Classes are still useful for reference semantics and Objective-C interop, but structs+protocols are preferred for most models.

swift
// POP: design around protocols, not inheritance
protocol Drawable {
    func draw(on canvas: Canvas)
}

extension Drawable {
    // Default implementation + polymorphism
    func drawTwice(on canvas: Canvas) {
        draw(on: canvas)
        draw(on: canvas)
    }
}

struct Circle: Drawable { let radius: Double
    func draw(on canvas: Canvas) { /* ... */ }
}
struct Square: Drawable { let side: Double
    func draw(on canvas: Canvas) { /* ... */ }
}

// Value types + protocols = no inheritance needed
let shapes: [any Drawable] = [Circle(radius: 5), Square(side: 3)]
shapes.forEach { $0.draw(on: canvas) }

// Retroactive modeling (extend types you don't own)
extension Int: Drawable {
    func draw(on canvas: Canvas) { /* draw the number */ }
}

Custom Protocol Witnesses

Protocols as dependencies (Repository) enable swapping implementations (real vs mock) for testing and flexibility. Associated types make protocols generic. This is Swift's dependency injection—inject a protocol-conforming type. The 'protocol witness' is the conforming type that provides concrete behavior. This pattern (repository, data source) is common in Swift architecture (VIPER, Clean Architecture).

swift
// Protocol with requirements
protocol Repository {
    associatedtype Entity
    func get(_ id: Int) -> Entity?
    func save(_ entity: Entity)
}

// Concrete implementation
struct UserRepo: Repository {
    typealias Entity = User
    func get(_ id: Int) -> User? { /* fetch */ nil }
    func save(_ user: User) { /* persist */ }
}

// Generic function using protocol
func display<R: Repository>(_ repo: R, id: Int) where R.Entity: CustomStringConvertible {
    if let entity = repo.get(id) {
        print(entity.description)
    }
}

// Test double for testing
struct MockUserRepo: Repository {
    typealias Entity = User
    var stored: [Int: User] = [:]
    func get(_ id: Int) -> User? { stored[id] }
    func save(_ user: User) { /* no-op for tests */ }
}
12

Generics Deep Dive

Generic Functions and Types

Generics write flexible, reusable code that works with any type while preserving type safety. T is a type parameter (placeholder). The compiler generates specialized versions for performance (no boxing). Generic types (Stack<T>) maintain their type parameter. Element is Array's generic type. Generics are resolved at compile time—no runtime overhead, unlike existentials.

swift
// Generic function
func swap<T>(_ a: inout T, _ b: inout T) {
    let temp = a; a = b; b = temp
}
var x = 1, y = 2
swap(&x, &y)  // x=2, y=1

// Generic type
struct Stack<T> {
    private var items: [T] = []
    mutating func push(_ item: T) { items.append(item) }
    mutating func pop() -> T? { items.popLast() }
}

var intStack = Stack<Int>()
intStack.push(42)
var strStack = Stack<String>()
strStack.push("hello")

// Generic method on non-generic type
extension Array {
    func chunked(into size: Int) -> [[Element]] {
        stride(from: 0, to: count, by: size).map {
            Array(self[$0..<Swift.min($0 + size, count)])
        }
    }
}

Type Constraints

Type constraints limit what types can be used: T: Equatable requires T to conform to a protocol, T: SomeClass requires a class hierarchy. The where clause adds more complex constraints (e.g., matching associated types). Constraints let you use the protocol's methods (== for Equatable, < for Comparable). Without constraints, you can only assign and pass T around—no operations.

swift
// Constraint: T must conform to Equatable
func indexOf<T: Equatable>(_ item: T, in array: [T]) -> Int? {
    for (i, elem) in array.enumerated() {
        if elem == item { return i }  // needs Equatable
    }
    return nil
}

// Multiple constraints
func max<T: Comparable>(_ a: T, _ b: T) -> T {
    return a > b ? a : b
}

// Constraint on associated type
protocol Repository {
    associatedtype Entity: Identifiable
    func find(_ id: Entity.ID) -> Entity?
}

// where clause for complex constraints
func merge<C1: Collection, C2: Collection>(
    _ c1: C1, _ c2: C2
) -> [C1.Element] where C1.Element == C2.Element {
    Array(c1) + Array(c2)
}

Opaque Types (some)

Opaque types (some Protocol, Swift 5.1) return a specific concrete type that's hidden from the caller. Unlike 'any' (existential), the type is fixed and known to the compiler—no boxing, compile-time dispatch. This is the foundation of SwiftUI's 'some View'. Use 'some' when you want to hide the concrete type but maintain performance. The caller can use protocol methods but can't rely on the specific type.

swift
// Opaque return type: returns ONE concrete type
// (hidden from caller, but fixed per call site)
func makeStack() -> some Collection<Int> {
    return [1, 2, 3]  // concrete type is [Int]
}

let s = makeStack()  // type is 'some Collection<Int>'
print(s.count)  // 3

// Useful for hiding implementation details
protocol Shape { func draw() }
struct Circle: Shape { func draw() { print("circle") } }

func makeShape() -> some Shape {
    return Circle()  // caller doesn't know it's Circle
}

// Difference from existential (any):
// - some: one concrete type, no boxing, compile-time dispatch
// - any: can be different types, runtime dispatch, boxing

// Opaque types in properties
struct View {
    var body: some View { Text("Hello") }  // SwiftUI
}

Conditional Conformance

Conditional conformance makes a type conform to a protocol only when its type parameters meet certain conditions. Array<Int> is Equatable because Int is; Array<MyStruct> isn't unless MyStruct is. This propagates: [[Int]] is Equatable because [Int] is. The standard library uses this extensively—Array, Optional, Dictionary all conditionally conform to Equatable, Hashable, Codable.

swift
// Array conforms to Equatable only if Element is Equatable
extension Array: Equatable where Element: Equatable {
    static func == (lhs: [Element], rhs: [Element]) -> Bool {
        return lhs.count == rhs.count && lhs.elementsEqual(rhs)
    }
}

[1, 2, 3] == [1, 2, 3]  // true (Int: Equatable)
// [SomeStruct] == [SomeStruct]  // error if SomeStruct isn't Equatable

// Conditional conformance for Hashable
extension Stack: Equatable where T: Equatable {
    static func == (lhs: Stack, rhs: Stack) -> Bool {
        return lhs.items == rhs.items
    }
}

// Enables: [Stack<Int>] == [Stack<Int>] (transitively)

Result and Generic Error Handling

Result<Success, Failure> is a generic enum for operations that can fail, carrying either a value or an error. It's useful when you want to defer error handling or store results. map transforms the success value; flatMap chains operations that can also fail. Result { try ... } converts throwing functions. .get() converts back to throwing. With async/await, Result is less necessary but still useful for stored/passed errors.

swift
// Result: built-in generic for success/failure
enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

func fetchUser(_ id: Int) -> Result<User, FetchError> {
    if id < 0 { return .failure(.invalidId) }
    return .success(User(id: id))
}

switch fetchUser(42) {
case .success(let user): print(user)
case .failure(let err): print(err)
}

// map/flatMap for chaining
let result = fetchUser(1)
    .map { $0.name }
    .flatMap { name in fetchAvatar(name) }

// Throwing function → Result
let result = Result { try throwingFunction() }
// Result → throwing
let value = try result.get()
13

ARC & Memory Management

Automatic Reference Counting (ARC)

ARC (Automatic Reference Counting) manages memory for class instances (reference types). Each strong reference increments the count; removing one decrements. When count hits 0, the object is freed immediately (deterministic, unlike garbage collection). Structs/enums (value types) don't use ARC—they're copied. ARC can't handle reference cycles—you must use weak/unowned to break them.

swift
class Person {
    let name: String
    init(name: String) { self.name = name; print("\(name) initialized") }
    deinit { print("\(name) deinitialized") }
}

var p1: Person? = Person(name: "Alice")  // "Alice initialized"
var p2 = p1  // strong ref count = 2
p1 = nil     // ref count = 1, not deinitialized
p2 = nil     // ref count = 0, "Alice deinitialized"

// ARC tracks strong references:
// - +1 on assignment/copy
// - -1 when variable goes out of scope
// - Object freed when count hits 0
// Unlike GC, deterministic (freed immediately when count=0)

Strong, Weak, and Unowned References

Strong references (default) keep objects alive. weak references don't keep the object alive and become nil when the object is freed (must be optional var). unowned references don't keep the object alive but are non-optional—use only when you're certain the referenced object outlives the reference (crash if accessed after dealloc). Use weak for delegate patterns; unowned for parent-child where parent always outlives child.

swift
class Person {
    var name: String
    var apartment: Apartment?  // strong
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class Apartment {
    var unit: String
    weak var tenant: Person?  // weak breaks cycle
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) deinit") }
}

var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice?.apartment = apt
apt?.tenant = alice  // weak, doesn't keep alice alive

alice = nil  // "Alice deinit" (no strong cycle)
apt = nil    // "Apartment 4A deinit"

// unowned: non-optional, assumed to always have a value
// Use when the referenced object outlives the reference
class Customer {
    var card: CreditCard?  // strong
}
class CreditCard {
    unowned let owner: Customer  // non-nil, non-optional
    init(owner: Customer) { self.owner = owner }
}

Retain Cycles and Closures

Closures capture references strongly by default. If a class stores a closure that captures self, you have a retain cycle (both keep each other alive → memory leak). Fix with capture lists: [weak self] (self becomes optional, use guard let) or [unowned self] (non-optional, crashes if nil). Always use weak self in closures stored as properties or passed to long-lived objects (observers, async tasks).

swift
class ViewModel {
    var name = "VM"
    var onUpdate: (() -> Void)?

    func setup() {
        // BAD: closure captures self strongly → cycle
        onUpdate = {
            print(self.name)  // strong capture
        }
        // ViewModel holds closure, closure holds ViewModel → leak!

        // GOOD: capture list with weak self
        onUpdate = { [weak self] in
            guard let self = self else { return }
            print(self.name)
        }

        // GOOD: unowned if self outlives closure
        onUpdate = { [unowned self] in
            print(self.name)
        }
    }
    deinit { print("VM deinit") }
}

Escaping and Non-escaping Closures

Non-escaping closures (default) run within the function and are discarded—no retain cycle risk, no need for self. in the closure. @escaping closures outlive the function (stored in a property, dispatched async, etc.)—they can cause retain cycles, so you must use self. explicitly and consider [weak self]. The compiler enforces this. Most completion handlers are @escaping; map/filter/reduce are non-escaping.

swift
class DataLoader {
    var storedHandler: (() -> Void)?

    // @escaping: closure outlives the function (stored or async)
    func load(with handler: @escaping (Data) -> Void) {
        storedHandler = { handler(cachedData) }  // stored
        DispatchQueue.global().async {
            handler(self.fetch())  // escapes via async
        }
    }

    // Non-escaping (default): closure runs during the function
    func process(_ transform: (Int) -> Int) -> [Int] {
        return [1, 2, 3].map(transform)  // runs now, no storage
    }
    // No need for [weak self]—self is alive during the call
}

// @escaping requires explicit self. in closures
class View {
    func load() {
        DataLoader().load { [self] data in  // self. required
            print(data)
        }
    }
}

Memory Safety and exclusivity

Swift enforces exclusivity—a variable can't be accessed (read+write or write+write) simultaneously from overlapping scopes. This prevents data races and undefined behavior. inout parameters get exclusive access for the call duration. Struct mutating methods hold exclusive access to self. Actors (Swift 5.5+) enforce exclusivity at the language level for shared mutable state, eliminating data races by design.

swift
// Exclusivity: Swift prevents overlapping access to a variable
// This is a compile-time or runtime error:
var step = 1
func increment(_ n: inout Int) {
    step += 1  // accessing global while n is inout to step
}
// increment(&step)  // ERROR: overlapping access

// Fix with explicit copy
func safeIncrement(_ n: inout Int) {
    var copy = step
    copy += 1
    step = copy
}

// Struct mutating methods have exclusive access
struct Counter {
    var count = 0
    mutating func increment(_ other: inout Counter) {
        count += other.count  // ok, different instances
    }
}

// 'inout' parameters: exclusive access for duration
// 'isolation' (Swift 5.5+): actors enforce exclusivity
14

SwiftUI Basics

Views and Modifiers

SwiftUI is Apple's declarative UI framework. Views are structs conforming to View with a 'body' property. Modifiers (.font, .padding) return new wrapped views—they don't mutate. Views are lightweight value types; SwiftUI diffs them to update the actual UI. The declarative style describes what the UI should look like for a given state, and SwiftUI handles the transitions.

swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("Hello, World!")
                .font(.title)
                .fontWeight(.bold)
                .foregroundColor(.blue)

            Button(action: { print("tapped") }) {
                HStack {
                    Image(systemName: "star.fill")
                    Text("Tap me")
                }
            }
            .padding()
            .background(Color.orange)
            .cornerRadius(10)
        }
        .padding()
    }
}

// Views are value types (structs), declarative
// Modifiers return new views (immutability)

State and Binding

@State is for local, mutable view state—when it changes, SwiftUI re-renders the view. @Binding lets a child view read/write a parent's @State via the $ prefix (creates a binding). State should be the single source of truth; pass bindings down, not values to modify. For shared/complex state across views, use @StateObject/@ObservedObject/@EnvironmentObject with ObservableObject.

swift
struct CounterView: View {
    @State private var count = 0  // local, mutable state

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") { count += 1 }
        }
    }
}

// @Binding: child receives a reference to parent's state
struct StepperView: View {
    @Binding var value: Int
    var body: some View {
        HStack {
            Button("-") { value -= 1 }
            Text("\(value)")
            Button("+") { value += 1 }
        }
    }
}

// Parent passes binding with $
struct ParentView: View {
    @State var score = 0
    var body: some View {
        StepperView(value: $score)  // $ creates a Binding
    }
}

ObservableObject and EnvironmentObject

ObservableObject (class) is for shared state across multiple views. @Published properties trigger view updates when changed. @StateObject creates and owns the object (use at the top of the hierarchy). @EnvironmentObject injects the object into the view tree—any descendant can access it without passing through bindings. @ObservedObject receives an existing object (doesn't own it). Use @StateObject for ownership, @ObservedObject/@EnvironmentObject for receiving.

swift
// Model: ObservableObject for shared state
class AppModel: ObservableObject {
    @Published var username: String = ""  // @Published triggers UI update
    @Published var isLoggedIn: Bool = false

    func login() {
        // ... auth logic ...
        isLoggedIn = true
    }
}

// @StateObject: owns the model (created once)
struct RootView: View {
    @StateObject var model = AppModel()
    var body: some View {
        // Inject into environment
        ContentView().environmentObject(model)
    }
}

// @EnvironmentObject: receives from environment
struct ProfileView: View {
    @EnvironmentObject var model: AppModel
    var body: some View {
        Text("Hello, \(model.username)")
    }
}

// @ObservedObject: receives from parent (doesn't own)
struct LoginView: View {
    @ObservedObject var model: AppModel
}

Lists and Navigation

List renders scrollable rows (like UITableView). Items must conform to Identifiable (or you provide id:). NavigationStack (iOS 16+) manages navigation; NavigationLink pushes destinations. LazyVStack/LazyHStack load content lazily for performance. ForEach is for repeating views inside other containers. Use List for tabular data, ScrollView+LazyVStack for custom layouts.

swift
struct Item: Identifiable {
    let id = UUID()
    let name: String
}

struct ListView: View {
    let items = [Item(name: "Apple"), Item(name: "Banana")]

    var body: some View {
        NavigationStack {
            List(items) { item in
                NavigationLink(item.name) {
                    DetailView(item: item)
                }
            }
            .navigationTitle("Fruits")
        }
    }
}

struct DetailView: View {
    let item: Item
    var body: some View {
        Text("Detail: \(item.name)")
            .navigationTitle(item.name)
    }
}

// ForEach for custom layouts
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            Text(item.name)
        }
    }
}

Forms and Sheets

Form automatically styles controls for settings screens (grouped, platform-appropriate). Common controls: TextField, Toggle, Slider, Picker, Stepper. .sheet presents a modal; .fullScreenCover for full-screen. Bindings ($) connect controls to state. Form adapts to platform (iOS grouped list, macOS form layout). Use Form for data entry and settings; use VStack for custom layouts.

swift
struct SettingsView: View {
    @State private var name = ""
    @State private var notifications = true
    @State private var volume: Double = 0.5
    @State private var theme = "Light"
    @State private var showSheet = false

    var body: some View {
        Form {
            Section("Profile") {
                TextField("Name", text: $name)
                Toggle("Notifications", isOn: $notifications)
            }
            Section("Audio") {
                Slider(value: $volume, in: 0...1)
            }
            Section("Appearance") {
                Picker("Theme", selection: $theme) {
                    Text("Light").tag("Light")
                    Text("Dark").tag("Dark")
                }
            }
            Button("Show Modal") { showSheet = true }
        }
        .sheet(isPresented: $showSheet) {
            Text("Modal content")
        }
    }
}
15

Closures Deep Dive

Closure Syntax Variations

Closures have many syntax forms. Full form: { (params) -> Type in body }. Type inference lets you omit types. Shorthand arguments ($0, $1) replace named params. Trailing closure syntax moves the closure outside () when it's the last arg. Multiple trailing closures (Swift 5.3+) name additional closures. Use the most concise form that's still readable—shorthand args are great for short closures like map.

swift
// Full closure
let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
    return a + b
}

// Type inference
let add2: (Int, Int) -> Int = { a, b in a + b }

// Shorthand argument names ($0, $1...)
let add3: (Int, Int) -> Int = { $0 + $1 }

// Trailing closure syntax
let result = [1, 2, 3].map { $0 * 2 }  // [2, 4, 6]

// Multiple trailing closures (Swift 5.3+)
Button {
    print("action")  // action
} label: {
    Text("Click")    // label
}

// No return for Void closures
let log: (String) -> Void = { msg in print(msg) }

Capturing Values

Closures capture variables from their enclosing scope. By default, they capture by reference (changes to the captured variable are visible). Capture lists [foo] capture by value (a snapshot at closure creation). For reference types (classes), [weak self] or [unowned self] break retain cycles. Value types (structs) captured by reference still see updates because the closure holds a box. Capture lists go before the parameter list.

swift
// Closures capture surrounding state
func makeIncrementer(amount: Int) -> () -> Int {
    var total = 0
    return {
        total += amount  // captures total and amount
        return total
    }
}

let inc = makeIncrementer(amount: 5)
inc()  // 5
inc()  // 10
// total persists between calls (captured by reference)

// Capture list controls how
class Foo { var x = 0 }
let foo = Foo()
let closure = { [foo] in  // captures foo by value (snapshot)
    print(foo.x)
}
foo.x = 100
closure()  // prints 0 (captured the old value)

// Without capture list, foo is captured by reference
let refClosure = { print(foo.x) }
foo.x = 200
refClosure()  // prints 200

Escaping and Autoclosure

@autoclosure wraps an expression in a zero-argument closure automatically, so callers write the expression directly (no braces). This enables lazy evaluation—assert() uses it to skip the condition in release builds. Combined with @escaping, you can defer evaluation. The && and || operators use @autoclosure for short-circuit evaluation. Use @autoclosure sparingly—it hides that code is deferred, which can confuse readers.

swift
// @autoclosure: wraps an expression in a closure automatically
func log(_ condition: @autoclosure () -> Bool, _ message: String) {
    if condition() {  // evaluated here, not at call
        print(message)
    }
}
log(2 > 1, "two is greater")  // no { } needed

// assert uses @autoclosure to avoid evaluating in release
assert(isValid, "Invalid state")  // isValid not checked in release

// @autoclosure + @escaping
func deferred(_ block: @autoclosure @escaping () -> Int) {
    DispatchQueue.main.async { print(block()) }
}
deferred(expensiveComputation())  // runs later, on main

// Short-circuit evaluation with @autoclosure
func &&(lhs: @autoclosure () -> Bool, rhs: @autoclosure () -> Bool) -> Bool {
    return lhs() ? rhs() : false  // rhs not evaluated if lhs is false
}

Higher-Order Functions

Swift's higher-order functions (map, filter, reduce, flatMap, compactMap) enable functional programming on collections. map transforms, filter selects, reduce aggregates, flatMap flattens, compactMap removes nils. These return new collections (immutability). Chaining composes operations. For large datasets, use .lazy to avoid intermediate arrays. These are the foundation of functional Swift—prefer them over imperative for loops when transforming data.

swift
let nums = [1, 2, 3, 4, 5, 6]

// map: transform each element
let doubled = nums.map { $0 * 2 }  // [2, 4, 6, 8, 10, 12]

// filter: keep elements matching predicate
let evens = nums.filter { $0 % 2 == 0 }  // [2, 4, 6]

// reduce: combine all into one
let sum = nums.reduce(0, +)  // 21
let product = nums.reduce(1, *)  // 720

// flatMap: transform + flatten
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 }  // [1, 2, 3, 4]

// compactMap: transform + filter nils
let strings = ["1", "abc", "3"]
let ints = strings.compactMap { Int($0) }  // [1, 3]

// Chaining (lazy evaluation)
let result = nums
    .filter { $0 % 2 == 0 }
    .map { $0 * $0 }
    .reduce(0, +)  // 4 + 16 + 36 = 56

Closures as Completion Handlers

Completion handlers were the standard async pattern before async/await (Swift 5.5). They're @escaping (run later) and often use Result for error handling. The downside is nested callbacks ('pyramid of doom'). async/await makes this linear and readable. New code should use async/await; completion handlers remain for delegate APIs and Objective-C interop. You can wrap completion handlers with withCheckedContinuation to use them with async/await.

swift
// Completion handler pattern (pre-async/await)
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data,
              let user = try? JSONDecoder().decode(User.self, from: data) else {
            completion(.failure(DecodeError()))
            return
        }
        completion(.success(user))
    }.resume()
}

// Usage (pyramid of doom)
fetchUser(id: 42) { result in
    switch result {
    case .success(let user):
        fetchAvatar(user) { avatarResult in
            // nested...
        }
    case .failure(let error):
        print(error)
    }
}

// Modern: async/await replaces this
func fetchUserAsync(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}
16

Property Wrappers

Defining a Property Wrapper

Property wrappers (@propertyWrapper) encapsulate reusable property behavior. The wrappedValue property is what users interact with. The init receives the initial value and any custom arguments. SwiftUI uses property wrappers extensively: @State, @Binding, @Published, @AppStorage. Define your own for validation, caching, logging, or default values. The wrapper is a struct/class that manages the storage.

swift
@propertyWrapper
struct Clamped<Value: Comparable> {
    var value: Value
    let range: ClosedRange<Value>

    var wrappedValue: Value {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
        self.range = range
    }
}

// Usage
struct Slider {
    @Clamped(0...100) var progress: Int = 50
}
var s = Slider()
s.progress = 150  // clamped to 100
s.progress = -10  // clamped to 0
print(s.progress)  // 0

Projected Value

The projectedValue (accessed via $) is a secondary interface to the property. SwiftUI uses this heavily: @State's $ gives a Binding for child views, @Published's $ gives a Publisher. Define projectedValue when the wrapper should expose something beyond the wrapped value (a binding, a publisher, the wrapper itself). The $ prefix syntax makes this ergonomic. Not all wrappers need a projectedValue—it's optional.

swift
@propertyWrapper
struct Observable<T> {
    var value: T
    var wrappedValue: T {
        get { value }
        set { value = newValue; onChange() }
    }
    var onChange: () -> Void = {}

    // projectedValue: accessed via $ (like @State's binding)
    var projectedValue: Observable<T> { self }
}

struct View {
    @Observable var count = 0
    // count: the wrapped value (Int)
    // $count: the projected value (Observable<Int>)
}

// SwiftUI's @State projects a Binding
struct Counter: View {
    @State var value = 0
    var body: some View {
        Stepper("Value: \(value)", value: $value)  // Binding<Int>
    }
}

Built-in Property Wrappers

SwiftUI provides many property wrappers: @State (local state), @Binding (parent state), @ObservedObject/@StateObject (external model), @EnvironmentObject (injected model), @AppStorage (UserDefaults), @SceneStorage (scene restoration), @FocusState (keyboard focus), @ScaledMetric (Dynamic Type), @Namespace (animations). Each manages storage and lifecycle. Combine's @Published triggers UI updates. Knowing which to use when is key to SwiftUI.

swift
import SwiftUI
import Combine

struct ContentView: View {
    @State var count = 0  // local view state

    @Binding var externalValue: Int  // parent-owned

    @EnvironmentObject var model: AppModel  // injected

    @AppStorage("username") var username = ""  // UserDefaults

    @SceneStorage("draft") var draft = ""  // per-scene storage

    @FocusState var isFocused: Bool  // focus management

    @ScaledMetric var size = 16  // Dynamic Type scaling

    @Namespace var animationNamespace  // matched geometry

    // Combine
    @Published var data = [Item]()  // in ObservableObject
}

// @AppStorage auto-syncs with UserDefaults
// @SceneStorage restores state per scene (iPad multitasking)

Custom Wrappers for Validation

Property wrappers shine for cross-cutting concerns. NonEmpty validates on set. Cached computes lazily and memoizes. Other ideas: Logged (logs changes), Trimmed (trims whitespace), Formatted (formats on get), Clamped (range limits), UserDefaults-backed. Wrappers reduce boilerplate—you write the logic once and apply it with @. They compose: @Logged @Clamped(0...100) var value. Keep wrappers focused on one concern.

swift
@propertyWrapper
struct NonEmpty {
    private var value: String = ""
    var wrappedValue: String {
        get { value }
        set {
            guard !newValue.isEmpty else {
                print("Warning: cannot set to empty")
                return
            }
            value = newValue
        }
    }
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}

@propertyWrapper
struct Cached<T> {
    private var cached: T?
    private var generator: () -> T
    var wrappedValue: T {
        mutating get {
            if cached == nil { cached = generator() }
            return cached!
        }
    }
    init(wrappedValue: @autoclosure @escaping () -> T) {
        self.generator = wrappedValue
    }
}

struct Config {
    @NonEmpty var name: String = "default"
    @Cached var expensiveValue: Int = computeExpensive()
}

Property Wrappers in Protocols and Generics

Property wrappers can be generic and used in protocols. However, they have limitations: a struct with wrapped properties doesn't get memberwise initializers (you must write init manually), and copying can be tricky (the wrapper is copied, including its state). For classes, this is less of an issue. Despite limitations, wrappers are powerful for declarative, reusable property behavior. SwiftUI's design relies heavily on them.

swift
// Apply to protocol requirements
protocol Configurable {
    @NonEmpty var name: String { get set }
}

// Generic property wrapper
@propertyWrapper
struct Validated<T> {
    private var value: T?
    let validator: (T) -> Bool
    var wrappedValue: T? {
        get { value }
        set {
            if let v = newValue, validator(v) { value = v }
        }
    }
    init(wrappedValue: T?, validator: @escaping (T) -> Bool) {
        self.validator = validator
        self.wrappedValue = wrappedValue
    }
}

struct Form {
    @Validated(validator: { $0.count >= 8 }) var password: String? = nil
}

// Limitation: property wrappers in structs can't be
// initialized from another instance easily (copy issues).
// They work best as stored properties, not computed.
17

Combine Framework

Publishers and Subscribers

Combine is Apple's reactive framework (like RxSwift). Publishers emit values; Subscribers receive them. sink creates a subscriber with closures. assign binds output to a property. Operators (map, filter, reduce) transform publishers declaratively. Combine is declarative—describe the pipeline, and values flow through. It's used for async operations, UI bindings, and event handling. Modern Swift prefers async/await, but Combine remains for complex pipelines.

swift
import Combine

// Publisher: emits values over time
let publisher = [1, 2, 3].publisher

// Subscriber: receives values
publisher.sink { value in
    print(value)  // 1, 2, 3
}

// With completion
publisher.sink(receiveCompletion: { completion in
    print("done: \(completion)")
}, receiveValue: { value in
    print(value)
})

// Assign to a property
class ViewModel {
    var data: String = ""
}
let vm = ViewModel()
Just("hello").assign(to: &vm.data)

// Operators transform publishers
[1, 2, 3, 4].publisher
    .map { $0 * 2 }
    .filter { $0 > 4 }
    .sink { print($0) }  // 6, 8

@Published and ObservableObject

@Published wraps a property and emits its new value to subscribers whenever it changes. The $ prefix accesses the publisher. This bridges Combine with SwiftUI—@Published in an ObservableObject triggers view updates. Subscriptions are cancelled when the AnyCancellable is deallocated, so store them (e.g., in a Set). Use Combine for debouncing, throttling, combining multiple async sources—things async/await handles less elegantly.

swift
class UserStore: ObservableObject {
    @Published var name: String = ""  // publishes on change
    @Published var age: Int = 0

    // $name is a Publisher<String, Never>
    func demo() {
        $name.sink { print("name changed to \($0)") }
        name = "Alice"  // prints "name changed to Alice"
    }
}

// Combine with SwiftUI
struct ProfileView: View {
    @ObservedObject var store: UserStore
    var body: some View {
        Text(store.name)  // updates when name changes
    }
}

// Cancellable: store subscriptions to keep them alive
class Service {
    var cancellables = Set<AnyCancellable>()
    let store = UserStore()

    init {
        store.$name
            .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
            .sink { name in saveToServer(name) }
            .store(in: &cancellables)  // keep alive
    }
}

Operators

Combine operators transform, filter, combine, and time-control publishers. map/scan transform; filter/selectors choose; merge/zip/combineLatest combine multiple streams; debounce/throttle control timing. Operators return new publishers (immutability, chaining). debounce waits for quiet period (search-as-you-type); throttle limits rate (button taps). collect gathers all values into an array. These enable declarative reactive pipelines.

swift
let publisher = [1, 2, 3, 4, 5].publisher

// Transforming
publisher.map { $0 * 2 }  // [2, 4, 6, 8, 10]
publisher.scan(0, +)  // [1, 3, 6, 10, 15] (running sum)

// Filtering
publisher.filter { $0 % 2 == 0 }  // [2, 4]
publisher.removeDuplicates()  // consecutive dupes
publisher.first()  // just 1

// Combining
let p1 = [1, 2].publisher
let p2 = [3, 4].publisher
p1.merge(with: p2)  // interleaved: 1, 2, 3, 4 (or any order)
p1.zip(p2)  // pairs: (1,3), (2,4)
p1.combineLatest(p2)  // latest of each

// Timing
publisher.delay(for: 1, scheduler: RunLoop.main)
publisher.throttle(for: 1, scheduler: .main, latest: true)
publisher.debounce(for: 0.5, scheduler: .main)
publisher.collect()  // gather all into [1,2,3,4,5]

Subjects (Manual Publishing)

Subjects are mutable publishers you can send values to manually—bridging imperative and reactive code. PassthroughSubject broadcasts without storing (event stream). CurrentValueSubject stores the latest value (state). New subscribers to CurrentValueSubject immediately get the current value. Use Subjects to wrap delegates, notifications, or UI events into Combine pipelines. @Published is essentially a CurrentValueSubject integrated with ObservableObject.

swift
import Combine

// PassthroughSubject: broadcasts to subscribers, no current value
let subject = PassthroughSubject<Int, Never>()
subject.sink { print($0) }  // subscriber 1
subject.send(1)  // prints 1
subject.send(2)  // prints 2

// CurrentValueSubject: holds current value, new subs get it
let current = CurrentValueSubject<Int, Never>(0)
current.sink { print($0) }  // prints 0 (current)
current.send(1)  // prints 1
current.sink { print($0) }  // prints 1 (current)
current.value  // 1

// Use cases:
// - PassthroughSubject: events (button taps, notifications)
// - CurrentValueSubject: state (like @Published but more control)
// - Bridge imperative code to Combine

// Finish a subject
subject.send(completion: .finished)

Error Handling

Combine publishers have a Failure type (Never for can't-fail). catch replaces an error with a fallback publisher. retry re-subscribes on failure (good for flaky networks). mapError transforms the error type. assertNoFailure crashes if an error occurs (use when you're certain). Publishers with Failure == Never can be used anywhere; those with errors need handling. This makes error handling explicit and composable in reactive pipelines.

swift
enum APIError: Error { case network, parse }

// Publishers can fail with an error
let publisher = Fail<Int, APIError>(error: .network)

// catch: replace error with fallback publisher
publisher
    .catch { _ in Just(0) }  // fallback to 0
    .sink { print($0) }  // 0

// retry: retry on failure
URLSession.shared.dataTaskPublisher(for: url)
    .retry(3)  // retry up to 3 times
    .sink(receiveCompletion: { _ in }, receiveValue: { _ in })

// mapError: transform error type
publisher
    .mapError { _ in NSError(domain: "x", code: 1) }

// assertNoFailure: crash if error (when you're sure it won't)
Just(1).assertNoFailure().sink { print($0) }

// Result publisher
let resultPublisher = [1, 2, 3].publisher.setFailureType(to: Error.self)
18

Access Control & Code Organization

Access Levels

Swift has five access levels. private (smallest scope, within declaration), fileprivate (within source file), internal (within module, default), public (anyone who imports), open (public + subclassable/overridable, classes only). Swift 5.9 adds package for SPM modules. Start restrictive and widen as needed. Use public for library APIs, internal for app code, private for implementation details. open is for framework classes designed for subclassing.

swift
// public: accessible from any module that imports this
public class PublicAPI {
    public func connect() {}
}

// internal (default): accessible within the module
class InternalClass {
    func helper() {}  // internal by default
}

// fileprivate: accessible within the source file
fileprivate func fileHelper() {}

// private: accessible within the enclosing declaration
class Counter {
    private var count = 0  // only Counter can access
    fileprivate func reset() { count = 0 }  // same file
}

// Swift 5.9: package (for Swift Package Manager)
package struct PackageType {
    package var value: Int
}

// Guidelines:
// - Start restrictive (private), open up as needed
// - public for framework API, internal for implementation
// - private for encapsulation within a type

Extensions and Organization

Extensions let you split a type's functionality across files (String+Validation.swift, String+Parsing.swift) and add conformance retroactively. This keeps files focused and manageable. Protocol conformance can be in a separate extension/file from the main type definition. Conditional conformance (where Element: ...) adds conformance conditionally. Use extensions to organize: keep the core type minimal, add functionality in focused extension files.

swift
// Split a type across files with extensions
// String+Validation.swift
extension String {
    var isValidEmail: Bool {
        contains("@") && contains(".")
    }
}

// String+Parsing.swift
extension String {
    func toURL() -> URL? { URL(string: self) }
}

// Conform to protocols in extensions
extension String: Comparable {  // already conforms, just showing
    // protocol methods
}

// Conditional conformance
extension Array: MyProtocol where Element: MyProtocol {}

// Protocol conformance in a separate file
// (keeps the main type definition clean)
struct User { let id: Int; let name: String }

extension User: Codable {}  // synthesized
extension User: Equatable {}  // synthesized
extension User: CustomStringConvertible {
    var description: String { "User(\(id), \(name))" }
}

Modules and Packages

Swift Package Manager (SPM) defines modules via Package.swift. Each target is a module—internal access is within a target, public crosses modules. Dependencies are other packages (git URLs). Products are what your package exposes. SPM is the modern way to manage Swift code (replacing CocoaPods/Carthage). Xcode integrates SPM natively. Structure: Sources/MyLibrary/ for code, Tests/MyLibraryTests/ for tests. Use modules to separate concerns and control access.

swift
// Package.swift (Swift Package Manager)
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyLibrary",
    products: [
        .library(name: "MyLibrary", targets: ["MyLibrary"]),
    ],
    dependencies: [
        .package(url: "https://github.com/.../Logging", from: "1.0.0"),
    ],
    targets: [
        .target(name: "MyLibrary", dependencies: ["Logging"]),
        .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
    ]
)

// Importing modules
import Foundation
import MyLibrary  // your module
import Logging    // dependency

// Each target is a module
// internal access is within a target/module
// public access crosses module boundaries

Initializers and Designated/Convenience

Classes have designated initializers (fully init, must call super's designated) and convenience initializers (delegate to another init in the same class). This two-tier system ensures complete initialization. Structs get a memberwise initializer for free. Failable initializers (init?) return nil on failure. Required initializers (required init) must be implemented by subclasses. For most code, prefer structs (simpler initialization) over classes.

swift
class Person {
    let name: String
    let age: Int

    // Designated initializer: fully initializes
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Employee: Person {
    let employeeId: String

    // Designated init must call super's designated init
    init(name: String, age: Int, employeeId: String) {
        self.employeeId = employeeId
        super.init(name: name, age: age)  // must call super
    }

    // Convenience init must call another init (same class)
    convenience init(name: String) {
        self.init(name: name, age: 0, employeeId: "TEMP")
    }
}

// Structs: memberwise initializer (free)
struct Point { let x, y: Int }
let p = Point(x: 1, y: 2)  // synthesized

// Failable initializers
struct Config {
    let port: Int
    init?(port: Int) {
        guard port > 0 else { return nil }
        self.port = port
    }
}

Optionals Deep Dive

Optionals are Swift's null-safety mechanism—an enum that's either .some(value) or .none (nil). You must unwrap to access the value. Optional chaining (?.) safely navigates; nil-coalescing (??) provides a default; if let/guard let safely unwrap. Avoid ! (force unwrap) unless you're certain—it crashes on nil. Optionals force you to handle absence explicitly, eliminating null-reference exceptions common in other languages.

swift
// Optional: enum with .some(Value) and .none
let maybeInt: Int? = 42
let nilInt: Int? = nil

// Optional chaining (short-circuits on nil)
let name: String? = user?.profile?.name  // nil if any link is nil

// Nil-coalescing (default value)
let value = maybeInt ?? 0  // 42
let value2 = nilInt ?? 0   // 0

// Forced unwrapping (crashes if nil)
let forced = maybeInt!  // 42
// let crash = nilInt!   // RUNTIME CRASH

// Optional binding
if let actual = maybeInt {
    print(actual)  // 42, actual is Int (not optional)
}

// Guard for early exit
func process(_ data: Data?) {
    guard let data = data else { return }
    // data is non-optional here
}

// Optional try
let result = try? parse(json)  // nil on error
let result2 = try! parse(json) // crashes on error

// Multiple binding
if let a = optA, let b = optB, a < b { /* ... */ }
19

Memory Management (ARC/weak)

ARC Basics

Automatic Reference Counting (ARC) tracks strong references. When count reaches zero, the object is deallocated. ARC is deterministic (unlike garbage collection): deinit runs immediately when the last reference is released. Most of the time, ARC just works. Issues arise with reference cycles: two objects strongly reference each other, preventing deallocation. Use weak or unowned to break cycles.

swift
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var reference1: Person?
var reference2: Person?

reference1 = Person(name: "Alice")
// Prints: Alice is being initialized

reference2 = reference1  // Strong reference count: 2

reference1 = nil  // Count: 1
reference2 = nil  // Count: 0, deinit runs
// Prints: Alice is being deinitialized

Weak References

weak references do not keep the referenced object alive. When the object is deallocated, weak references automatically become nil. weak must be optional (var tenant: Person?). Use weak when the referenced object has a shorter lifetime (tenant may leave the apartment). The classic use case is the delegate pattern: the delegate is weak to avoid cycles. weak has slight overhead (registered with the runtime for nil-setting).

swift
class Person {
    var name: String
    var apartment: Apartment?  // Strong (person owns apartment)
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class Apartment {
    var unit: String
    weak var tenant: Person?  // Weak (breaks cycle)
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) deinit") }
}

var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")

alice!.apartment = apt
apt!.tenant = alice  // Weak reference, no cycle

alice = nil  // Alice deinit (apartment's tenant becomes nil)
apt = nil    // Apartment deinit

// weak must be optional (becomes nil when deallocated)

Unowned References

unowned references, like weak, do not keep the object alive. Unlike weak, unowned is non-optional and does not become nil. Accessing an unowned reference after deallocation crashes. Use unowned when you can guarantee the referenced object outlives the reference (e.g., a credit card cannot exist without its customer). unowned has less overhead than weak. Choose: weak for uncertain lifetimes, unowned for guaranteed lifetimes.

swift
class Customer {
    let name: String
    var card: CreditCard?  // Strong
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class CreditCard {
    let number: UInt
    unowned let customer: Customer  // Non-optional, no cycle
    init(number: UInt, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card \(number) deinit") }
}

var john: Customer? = Customer(name: "John")
john!.card = CreditCard(number: 1234, customer: john!)

john = nil
// Customer deinit, then CreditCard deinit

// unowned: non-optional, assumed to always have a value
// CRASH if accessed after deallocation
// Use when the referenced object has same or longer lifetime

Closures & Capture Lists

Closures capture references strongly by default, causing cycles when the closure is stored on self. Capture lists ([weak self] or [unowned self]) break the cycle. weak self requires optional handling (guard let self = self). Capture specific values (let name = self.name) to avoid capturing self entirely. Closures passed to functions (not stored) do not cause cycles. Always use capture lists for stored closures that reference self.

swift
class ViewController {
    var name = "MyVC"
    var onComplete: (() -> Void)?

    // BAD: Strong reference cycle
    func setupBad() {
        onComplete = {
            print("\(self.name) completed")  // Captures self strongly
        }
    }

    // GOOD: Capture list breaks cycle
    func setupGood() {
        onComplete = { [weak self] in
            guard let self = self else { return }
            print("\(self.name) completed")
        }
    }

    // unowned for guaranteed lifetime
    func setupUnowned() {
        onComplete = { [unowned self] in
            print("\(self.name) completed")
        }
    }

    // Capture specific values
    func setupCapture() {
        let name = self.name  // Capture value, not self
        onComplete = {
            print("\(name) completed")
        }
    }

    deinit { print("VC deinit") }
}

Detecting & Fixing Leaks

Common leak sources: delegates stored strong (use weak), Timer retaining target (invalidate in deinit), NotificationCenter observers (remove in deinit). The Xcode Memory Graph Debugger visualizes object graphs and highlights leaks. Run with MallocStackLogging to get allocation traces. Test deinit by setting references to nil and verifying deinit runs. Instruments (Leaks tool) detects leaks at runtime. Always pair setup with teardown.

swift
// Xcode memory graph debugger: visualize reference cycles
// Product > Debug > View Memory Graph Hierarchy

// Common leak patterns:
// 1. Delegate stored as strong
class View {
    weak var delegate: ViewDelegate?  // MUST be weak
}

// 2. Timer retains target
class TimerHolder {
    var timer: Timer?
    func start() {
        // BAD: timer retains self via #selector
        timer = Timer.scheduledTimer(timeInterval: 1,
            target: self, selector: #selector(fire),
            userInfo: nil, repeats: true)
    }
    @objc func fire() {}
    deinit { timer?.invalidate() }  // MUST invalidate
}

// 3. NotificationCenter observer
class Observer {
    init() {
        NotificationCenter.default.addObserver(
            self, selector: #selector(handle),
            name: .someNotification, object: nil)
    }
    @objc func handle() {}
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

// Use Xcode's Debug Memory Graph to find leaks
20

Actors & async/await

async/await Basics

async/await (Swift 5.5+) makes asynchronous code look synchronous. async marks functions that can suspend; await marks suspension points. Tasks bridge sync to async contexts. async let starts a concurrent task; await collects the result. Use async let for parallel execution, regular await for sequential. Errors propagate with throws/try. The compiler enforces await at suspension points. async/await replaces completion handlers and Combine for many use cases.

swift
// Async function
func fetchUser(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// Calling async functions
func loadProfile() async {
    do {
        let user = try await fetchUser(id: 1)
        print("Loaded: \(user.name)")
    } catch {
        print("Error: \(error)")
    }
}

// Task: bridge async to sync context
Task {
    await loadProfile()
}

// Concurrent execution
func loadMultiple() async {
    async let user1 = fetchUser(id: 1)
    async let user2 = fetchUser(id: 2)
    async let user3 = fetchUser(id: 3)

    let users = try await [user1, user2, user3]
    print("Loaded \(users.count) users")
}

// Sequenced vs concurrent
let sequential = try await fetchUser(id: 1)  // Waits
let concurrent = try await fetchUser(id: 2)  // Then waits again

Actors

Actors (Swift 5.5+) are reference types with serialized access, preventing data races. Only one method executes on an actor at a time—no manual locking needed. Actor methods must be called with await (they may suspend). Properties are isolated and cannot be accessed directly from outside. Actors are the safe default for shared mutable state in concurrent code. Use actors instead of classes with locks for most concurrent state.

swift
// Actor: reference type with serialized access
actor BankAccount {
    private var balance: Decimal

    init(balance: Decimal) {
        self.balance = balance
    }

    func deposit(_ amount: Decimal) {
        balance += amount
    }

    func withdraw(_ amount: Decimal) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }

    func getBalance() -> Decimal { balance }
}

// Usage (must be async)
let account = BankAccount(balance: 100)
Task {
    await account.deposit(50)
    let balance = await account.getBalance()
    print("Balance: \(balance)")  // 150
}

// Actors prevent data races automatically
// Only one method runs at a time per actor instance
// No need for locks or queues

Actor Isolation

Actor isolation protects state. nonisolated members can be accessed without await (for pure values or methods that do not touch isolated state). @MainActor is a global actor for main-thread isolation (UI updates). @globalActor creates custom global actors. Crossing actor boundaries requires await. The compiler verifies isolation at compile time, preventing data races. Use @MainActor for ViewModels and UI code; custom actors for domain-specific isolation.

swift
actor Counter {
    private var count = 0

    // Isolated method (default)
    func increment() { count += 1 }

    // Nonisolated: can be called without await
    nonisolated var description: String { "Counter" }

    // nonisolated function
    nonisolated func id() -> String { "counter-\(ObjectIdentifier(self))" }
}

// Global actor: @MainActor runs on main thread
@MainActor
class ViewModel: ObservableObject {
    @Published var data: [Item] = []

    func loadData() async {
        // Always runs on main thread
        data = try await fetchItems()
    }
}

// Mark specific functions
class Service {
    @MainActor
    func updateUI(_ result: Result) {
        // UI updates on main thread
    }
}

// globalActor lets you apply to types, functions, properties
@globalActor
actor MyActor {
    static let shared = MyActor()
}

Structured Concurrency

Task groups enable dynamic parallel computation. addTask spawns child tasks; for await group collects results as they complete. Task groups enforce structured concurrency: all children complete before the parent continues. Cancellation propagates from parent to children automatically. Check Task.isCancelled in long-running tasks. withThrowingTaskGroup propagates errors, cancelling siblings on throw. Use task groups for fan-out/fan-in patterns.

swift
// TaskGroup: dynamic parallel computation
func fetchAllUsers(ids: [Int]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask { try await fetchUser(id: id) }
        }

        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

// Task tree: parent-child relationship
func process() async {
    await withTaskGroup(of: Void.self) { group in
        for i in 1...5 {
            group.addTask {
                await processItem(i)
            }
        }
        // All tasks complete before group returns
    }
    print("All done")
}

// Cancellation
func longRunning() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            for i in 1...100 {
                if Task.isCancelled { return }
                await doWork(i)
            }
        }
    }
}

// Cancel from outside
let task = Task { await longRunning() }
task.cancel()

AsyncSequence & Streams

AsyncSequence is the async version of Sequence, enabling for await loops. AsyncStream bridges callback-based or delegate-based APIs to async/await. yield emits values, finish terminates. onTermination cleans up resources. AsyncThrowingStream supports errors. Use AsyncSequence for paginated APIs, real-time data, or any stream of values over time. The standard library provides .lines on URL for line-by-line file reading. AsyncSequence integrates with task cancellation.

swift
// AsyncSequence: iterate asynchronously
struct Counter: AsyncSequence {
    struct AsyncIterator: AsyncIteratorProtocol {
        var current: Int
        let max: Int
        mutating func next() async -> Int? {
            guard current <= max else { return nil }
            defer { current += 1 }
            try? await Task.sleep(nanoseconds: 100_000_000)
            return current
        }
    }

    let start: Int
    let max: Int

    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(current: start, max: max)
    }
}

// Usage
for await num in Counter(start: 1, max: 5) {
    print(num)  // 1 2 3 4 5 (with delays)
}

// AsyncStream: bridge callbacks to async
func urlLines(_ url: URL) -> AsyncThrowingStream<String, Error> {
    AsyncThrowingStream { continuation in
        let task = Task {
            do {
                for try await line in url.lines {
                    continuation.yield(line)
                }
                continuation.finish()
            } catch {
                continuation.finish(throwing: error)
            }
        }
        continuation.onTermination = { _ in task.cancel() }
    }
}
21

Testing

XCTest Basics

XCTest is the standard testing framework. XCTestCase groups related tests. setUp/tearDown run before/after each test. Assertions: XCTAssertEqual, XCTAssertNil, XCTAssertThrowsError, XCTAssertTrue. @testable import accesses internal symbols. Tests run on the main thread by default. Use XCTAssertNotNil for optionals. Run tests with Cmd+U. Xcode shows test results inline. Write tests alongside code for fast feedback.

swift
import XCTest
@testable import MyApp

class UserTests: XCTestCase {
    var user: User!

    override func setUp() {
        super.setUp()
        user = User(name: "Alice", age: 30)
    }

    override func tearDown() {
        user = nil
        super.tearDown()
    }

    func testName() {
        XCTAssertEqual(user.name, "Alice")
    }

    func testAgeAfterBirthday() {
        user.haveBirthday()
        XCTAssertEqual(user.age, 31)
    }

    func testValidation() {
        XCTAssertThrowsError(try User(name: "", age: -1)) { error in
            guard let error = error as? UserError else {
                XCTFail("Wrong error type")
                return
            }
            XCTAssertEqual(error, .invalidName)
        }
    }

    func testOptional() {
        XCTAssertNotNil(user.email)
        XCTAssertNil(user.address)
    }
}

// Run: Cmd+U in Xcode

Async Testing

Swift 5.5+ supports async test methods directly (async throws func test...). Use try await inside tests. For older code, use XCTestExpectation: create expectation, fulfill() in callback, wait(for:timeout:). Set reasonable timeouts to catch hangs. For testing async sequences, use for await loops. Mock network calls with URLProtocol or dependency injection. Test both success and failure paths. Async tests run concurrently by default—use .serialized trait if order matters.

swift
class APITests: XCTestCase {
    func testFetchUser() async throws {
        let user = try await fetchUser(id: 1)
        XCTAssertEqual(user.name, "Alice")
    }

    func testFetchFailure() async {
        do {
            _ = try await fetchUser(id: -1)
            XCTFail("Should have thrown")
        } catch {
            // Expected
        }
    }

    // Old style with expectations (still works)
    func testAsyncWithExpectation() {
        let expectation = XCTestExpectation(description: "Fetch completes")

        fetchUserAsync { result in
            switch result {
            case .success(let user):
                XCTAssertEqual(user.name, "Alice")
            case .failure:
                XCTFail("Should succeed")
            }
            expectation.fulfill()
        }

        wait(for: [expectation], timeout: 5)
    }
}

Mocking & Stubbing

Dependency injection enables testing: define protocols for dependencies, inject mocks in tests. MockNetworkService returns predetermined results. Test success and failure cases by setting mock.fetchUserResult. This pattern decouples tests from the network, making them fast and deterministic. For complex mocks, use libraries like Cuckoo or Mockingbird. Test the public API of your types, not implementation details. Mocks should be simple and focused.

swift
// Protocol for dependency injection
protocol NetworkService {
    func fetchUser(id: Int) async throws -> User
}

// Production implementation
class APIService: NetworkService {
    func fetchUser(id: Int) async throws -> User {
        // Real network call
    }
}

// Mock for testing
class MockNetworkService: NetworkService {
    var fetchUserResult: Result<User, Error>?

    func fetchUser(id: Int) async throws -> User {
        switch fetchUserResult {
        case .success(let user): return user
        case .failure(let error): throw error
        case .none: throw URLError(.badURL)
        }
    }
}

class ViewModelTests: XCTestCase {
    func testLoadUser() async throws {
        let mock = MockNetworkService()
        mock.fetchUserResult = .success(User(name: "Alice", age: 30))

        let vm = ViewModel(service: mock)
        await vm.load()

        XCTAssertEqual(vm.userName, "Alice")
    }
}

UI Testing

UI tests automate user interactions via accessibility identifiers. XCUIApplication launches the app. Find elements by accessibility identifier (set in code with .accessibilityIdentifier). Actions: tap, typeText, swipeLeft/Right. Assertions: waitForExistence, exists, hittable. UI tests are slower than unit tests but catch integration bugs. Set accessibility identifiers in SwiftUI with .accessibilityIdentifier("Email"). Run UI tests on multiple devices/simulators for coverage.

swift
import XCTest

class AppUITests: XCTestCase {
    override func setUpWithError() throws {
        continueAfterFailure = false
        let app = XCUIApplication()
        app.launch()
    }

    func testLoginFlow() {
        let app = XCUIApplication()

        let emailField = app.textFields["Email"]
        let passwordField = app.secureTextFields["Password"]
        let loginButton = app.buttons["Log In"]

        emailField.tap()
        emailField.typeText("[email protected]")

        passwordField.tap()
        passwordField.typeText("password123")

        loginButton.tap()

        // Verify navigation
        XCTAssertTrue(app.staticTexts["Welcome, Alice!"].waitForExistence(timeout: 5))
    }

    func testSwipeAndTap() {
        let cell = app.cells["Item 5"]
        cell.swipeLeft()
        app.buttons["Delete"].tap()
        XCTAssertFalse(cell.exists)
    }
}

Performance & Code Coverage

measure benchmarks code performance, comparing against baselines. Set baselines in Xcode; tests fail if regression exceeds threshold. measureMetrics tracks specific metrics (peak memory, CPU). Enable code coverage in the scheme to see untested code paths. Aim for 80%+ coverage on business logic. Snapshot testing (swift-snapshot-testing) captures UI for visual regression. Profile with Instruments (Time Profiler, Allocations) for deeper analysis. Performance tests catch regressions early.

swift
class PerformanceTests: XCTestCase {
    func testSortingPerformance() {
        let data = (1...10000).shuffled()

        measure {
            _ = data.sorted()
        }
    }

    func testMemoryUsage() {
        measureMetrics([XCTPerformanceMetric.peakMemory]) {
            _ = (0..<10000).map { _ in [Int](repeating: 0, count: 100) }
        }
    }

    // Baseline comparison
    func testWithBaselines() {
        let options = XCTMeasureOptions()
        options.defaultOptions.measurementUnits = .seconds

        measure(options) {
            // Code to benchmark
        }
    }
}

// Code coverage: enable in Edit Scheme > Test > Options
// View coverage: Cmd+Shift+Y (Report Navigator) > Coverage

// Snapshot testing (third-party)
// func testAppearance() {
//     assertSnapshot(of: view, as: .image)
// }
22

Swift Concurrency

async/await

async/await (Swift 5.5+) simplifies async code. async marks functions that can suspend. await marks suspension points. Task creates an async context. Much cleaner than completion handlers. Errors propagate with try.

swift
func fetchData() async throws -> String {
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8) ?? ""
}
// Call from async context
Task {
    let result = try await fetchData()
    print(result)
}

Actors

actor (Swift 5.5+) is a reference type with automatic mutual exclusion. Only one task accesses its state at a time. Methods are implicitly async when called from outside. Replaces locks and dispatch queues for shared mutable state. Thread-safe by design.

swift
actor Counter {
    private var count = 0
    func increment() {
        count += 1
    }
    func getValue() -> Int {
        count
    }
}
// Usage
Task {
    let counter = Counter()
    await counter.increment()
    let value = await counter.getValue()
}

Async Sequence

AsyncSequence is the async equivalent of Sequence. for await iterates asynchronously. Useful for streaming data (network, file lines). Implement AsyncIteratorProtocol to create custom async sequences. The compiler handles suspension and cancellation.

swift
for try await item in asyncSequence {
    print(item)
}
// Custom AsyncSequence
struct Counter: AsyncSequence {
    struct AsyncIterator: AsyncIteratorProtocol {
        var current = 0
        mutating func next() async -> Int? {
            current += 1
            return current <= 5 ? current : nil
        }
    }
    func makeAsyncIterator() -> AsyncIterator { AsyncIterator() }
}

Task Groups

Task groups run multiple tasks concurrently and collect results. addTask adds a child task. for await iterates results as they complete. All tasks must complete before the group returns. Structured concurrency: cancellation propagates to children. Results are collected safely.

swift
let results = await withTaskGroup(of: Int.self) { group in
    for i in 1...5 {
        group.addTask { i * i }
    }
    var sum = 0
    for await result in group {
        sum += result
    }
    return sum
}

Continuation

withCheckedContinuation bridges completion-handler APIs to async/await. resume(returning:) resumes the async function. Must be called exactly once. withCheckedThrowingContinuation supports errors. Useful for adopting async/await with existing APIs.

swift
func fetchWithCompletion(_ completion: @escaping (String) -> Void) {
    // Legacy completion handler API
    completion("data")
}
// Wrap in async
func fetchAsync() async -> String {
    await withCheckedContinuation { continuation in
        fetchWithCompletion { result in
            continuation.resume(returning: result)
        }
    }
}
23

SwiftUI Deep

View Modifiers

Modifiers wrap views to change appearance or behavior. Order matters: later modifiers wrap earlier ones. .padding before .background puts padding inside the background. Modifiers return new View instances. Chain them for complex styling.

swift
Text("Hello")
    .font(.title)
    .foregroundColor(.blue)
    .padding()
    .background(Color.gray)
    .cornerRadius(8)
    .shadow(radius: 4)

Lists

List displays scrollable rows. ForEach generates rows from data. onDelete enables swipe-to-delete. Other modifiers: onMove, onInsert. Identifiable data (id property) is required for ForEach. List style: .plain, .insetGrouped, .sidebar.

swift
List {
    ForEach(items) { item in
        HStack {
            Text(item.name)
            Spacer()
            Text("\(item.price)")
        }
    }
    .onDelete { indexSet in
        items.remove(atOffsets: indexSet)
    }
}
.listStyle(.insetGrouped)

Navigation

NavigationStack manages a stack of views. NavigationLink pushes a destination. navigationTitle sets the title. toolbar adds navigation bar buttons. For iOS 16+. Use NavigationView for older versions. Sheets and alerts use .sheet and .alert modifiers.

swift
NavigationStack {
    List(items) { item in
        NavigationLink(item.name) {
            DetailView(item: item)
        }
    }
    .navigationTitle("Items")
    .toolbar {
        ToolbarItem(placement: .navigationBarTrailing) {
            Button("Add") { addItem() }
        }
    }
}

State Management

@State for local view state (value types). @Binding for passed-in state. @StateObject for owned observable objects (created once). @ObservedObject for external observable objects. @EnvironmentObject for app-wide state. @State changes trigger view updates.

swift
struct CounterView: View {
    @State private var count = 0
    @Binding var selected: Int
    @StateObject var viewModel = ViewModel()
    var body: some View {
        Button("\(count)") { count += 1 }
    }
}

Animations

withAnimation animates state changes. .spring, .easeInOut are animation curves. .animation(value:) animates when the value changes. .scaleEffect, .opacity, .offset are animatable. Implicit animations use .animation modifier. Explicit use withAnimation.

swift
struct AnimatedView: View {
    @State private var scale: CGFloat = 1.0
    var body: some View {
        Button("Tap") {
            withAnimation(.spring(response: 0.3)) {
                scale = scale == 1 ? 1.5 : 1
            }
        }
        .scaleEffect(scale)
        .animation(.easeInOut(duration: 0.3), value: scale)
    }
}
24

Combine Framework

Publishers & Subscribers

Combine is Apples reactive framework. Publishers emit values over time. Subscribers receive them. sink creates a subscriber. Just emits a single value. PassthroughSubject is a manual publisher. Cancellables must be retained or the subscription cancels. Similar to RxSwift.

swift
import Combine

let publisher = Just(42)
let cancellable = publisher.sink { value in
    print("Received: \(value)")
}
// Just emits one value then finishes
// PassthroughSubject: manual values
let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)

Operators

Operators transform publisher output. map transforms values. filter selects values. debounce delays until quiet. throttle limits rate. combineLatest merges streams. flatMap chains publishers. Operators are lazy: nothing happens until sink subscribes.

swift
let publisher = (1...10).publisher
    .map { $0 * 2 }
    .filter { $0 > 10 }
    .sink { print($0) }  // 12, 14, 16, 18, 20

// Debounce
let debounced = subject
    .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
    .sink { print($0) }

@Published

@Published exposes a property as a Combine publisher. $prefix accesses the publisher. Changes emit new values. Works with ObservableObject for SwiftUI. The publisher emits the current value on subscription. Useful for reactive UI updates without manual notifications.

swift
class ViewModel: ObservableObject {
    @Published var count = 0
}
let vm = ViewModel()
let cancellable = vm.$count.sink { newCount in
    print("Count changed: \(newCount)")
}
vm.count = 1  // Prints "Count changed: 1"
vm.count = 2  // Prints "Count changed: 2"

Future

Future wraps a completion-handler API as a publisher. promise(.success) emits a value. promise(.failure) emits an error. Future emits exactly once. Useful for bridging callback APIs to Combine. The closure runs eagerly unless wrapped with .delay or deferred.

swift
func fetchUser() -> Future<User, Error> {
    Future { promise in
        URLSession.shared.dataTask(with: url) { data, _, error in
            if let error = error { promise(.failure(error)) }
            else { promise(.success(parse(data!))) }
        }.resume()
    }
}
// Usage
fetchUser().sink(receiveCompletion: { _ in }, receiveValue: { user in print(user) })

Error Handling

Fail immediately emits an error. catch replaces a failed publisher with another. retry resubscribes on failure. assertNoFailure crashes on error (for debugging). The completion event signals no more values. Errors propagate downstream unless caught.

swift
let publisher = Fail<Int, MyError>(error: .notFound)
    .catch { error in
        Just(0)  // Fallback value
    }
    .sink { print($0) }  // 0

// retry on failure
let retried = requestPublisher
    .retry(3)
    .sink(receiveCompletion: { _ in }, receiveValue: { _ in })
25

Memory Management

ARC Basics

ARC (Automatic Reference Counting) tracks references. When count hits zero, deinit runs and memory frees. Strong references increment the count. Reference cycles prevent deallocation. ARC is deterministic (unlike GC). deinit runs synchronously when the last reference drops.

swift
class Person {
    let name: String
    init(name: String) { self.name = name }
    deinit { print("\(name) is being deinitialized") }
}
var p1: Person? = Person(name: "Alice")
p1 = nil  // Prints "Alice is being deinitialized"

Weak References

weak references do not increment retain count. They auto-set to nil when the object deallocates. Must be optional var. Use for delegates and observers to break cycles. weak references are zeroing: safe to access after the object is gone. The most common fix for retain cycles.

swift
class View {
    weak var delegate: Delegate?
}
class Delegate {
    var view: View?  // strong
}
// weak: does not keep object alive, auto-set to nil
// Use for delegate patterns to avoid cycles

Unowned References

unowned references do not increment retain count. Unlike weak, they are non-optional and not zeroing. Accessing after dealloc crashes. Use when the referenced object outlives or dies with the referencer. Faster than weak (no nil check). Common in closures capturing self when self outlives the closure.

swift
class Customer {
    var card: CreditCard?
}
class CreditCard {
    unowned let customer: Customer
    init(customer: Customer) { self.customer = customer }
}
// unowned: does not keep alive, but not optional
// CRASHES if accessed after dealloc
// Use when the other object has same or shorter lifetime

Retain Cycles

Retain cycles occur when objects reference each other strongly. Neither deallocates, leaking memory. Fix with weak or unowned. Common in delegates, observers, and closures. Closures capture self strongly by default. [weak self] or [unowned self] breaks the cycle. Use Memory Graph Debugger to find cycles.

swift
// BAD: retain cycle
class Node {
    var next: Node?
    var prev: Node?  // strong -> cycle
}
// GOOD: break with weak
class Node {
    var next: Node?
    weak var prev: Node?
}
// Closures: use [weak self]
{ [weak self] in self?.update() }

Closures & Capture

Closures capture variables by reference. Stored closures (escaping) create retain cycles if they capture self. [weak self] makes the capture optional and weak. @escaping marks closures that outlive the function call. Non-escaping closures (default) cannot cause cycles. The compiler warns about potential cycles.

swift
class Timer {
    var handler: (() -> Void)?
    func start() {
        handler = { [weak self] in
            self?.tick()  // weak to avoid cycle
        }
    }
    func tick() { /* ... */ }
}
// Escaping closure: stored for later
// @escaping marks escaping closures
26

Error Handling Deep

Custom Errors

Custom errors conform to Error. LocalizedError provides errorDescription. Associated values carry context. Enums are the idiomatic way to define errors. Each case represents a distinct failure. Switch exhaustiveness ensures all errors are handled. Conform to CustomStringConvertible for debug output.

swift
enum APIError: Error, LocalizedError {
    case invalidURL
    case unauthorized
    case serverError(Int)
    case decodingFailed(Error)

    var errorDescription: String? {
        switch self {
        case .invalidURL: return "Invalid URL"
        case .unauthorized: return "Unauthorized"
        case .serverError(let code): return "Server error: \(code)"
        case .decodingFailed(let err): return "Decode failed: \(err)"
        }
    }
}

Result Type

Result is a typed success/failure enum. Useful for synchronous operations that can fail. map transforms the success value. flatMap chains operations. get() throws to convert to try/catch. Result is preferred over throwing functions for stored or passed-around results. Combines error handling with value semantics.

swift
func fetchUser(id: Int) -> Result<User, APIError> {
    guard id > 0 else { return .failure(.invalidURL) }
    return .success(User(name: "Alice"))
}
// Usage
switch fetchUser(id: 1) {
case .success(let user): print(user)
case .failure(let error): print(error)
}
// map and flatMap
let result = fetchUser(id: 1).map { $0.name }

Rethrows

rethrows propagates errors from a closure parameter. If the closure does not throw, the function does not throw either. This avoids forcing try on callers that pass non-throwing closures. Used by map, filter, and other higher-order functions. The function itself cannot throw independently of the closure.

swift
func withLock<T>(_ lock: NSLock, _ body: () throws -> T) rethrows -> T {
    lock.lock()
    defer { lock.unlock() }
    return try body()
}
// Only throws if body throws
let x = withLock(lock) { 5 }  // no try needed
let y = try withLock(lock) { try throwingFunc() }

Error Propagation

try propagates errors to the caller. try? converts errors to nil (returns Optional). try! crashes on error (use when certain). Errors bubble up the call stack until caught. Functions that throw must be marked throws. The compiler enforces try markers, preventing unhandled errors. defer runs regardless of thrown errors.

swift
func process() throws {
    let data = try fetchData()  // may throw
    let user = try parse(data)  // may throw
    save(user)                  // does not throw
}
// try?: converts to optional, nil on error
let user = try? fetchUser()
// try!: asserts no error, crashes if thrown
let user = try! fetchUser()

Cleanup with defer

defer schedules cleanup to run when the scope exits. Runs regardless of how the scope exits (return, throw, or fall-through). Multiple defers run in LIFO order. Useful for closing files, releasing locks, freeing resources. defer cannot throw or break/continue. The closure captures variables by reference.

swift
func processFile() throws {
    let file = open("file.txt")
    defer { close(file) }  // runs on scope exit

    let data = try read(file)
    // ... process ...
    // close() runs here even if read throws
}
// Multiple defers run in reverse order (LIFO)
defer { print("1") }
defer { print("2") }  // prints 2 then 1

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.