Advanced Swift

Closures, generics, error handling with throws/try/catch, and ARC and retain cycles.

Closures

A closure is a self-contained block of functionality that can be passed around and executed later — Swift's version of a lambda/anonymous function, and functions themselves are just closures with a name.

Swift
let multiply = { (a: Int, b: Int) -> Int in
    a * b
}

print(multiply(3, 4))   // 12

Closures are used constantly with higher-order functions like map, filter, and sorted:

Swift
let numbers = [5, 2, 8, 1, 9]

let doubled = numbers.map { $0 * 2 }              // [10, 4, 16, 2, 18] — $0 is shorthand for the first argument
let evens = numbers.filter { $0 % 2 == 0 }          // [2, 8]
let sorted = numbers.sorted { $0 < $1 }              // [1, 2, 5, 8, 9]

Trailing closure syntax

When a closure is a function's last argument, Swift lets you write it outside the parentheses — this is the idiomatic style you'll see constantly in real Swift/SwiftUI code:

Swift
func performRequest(url: String, completion: (String) -> Void) {
    completion("Response from \(url)")
}

performRequest(url: "api.example.com") { response in
    print(response)   // Response from api.example.com
}

Closures also capture variables from their surrounding scope by reference, which is what makes them genuinely powerful (and occasionally a source of subtle bugs, covered in the ARC section below):

Swift
func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}

let counter = makeCounter()
print(counter())   // 1
print(counter())   // 2 — count was captured and persists between calls

Generics

Generics let you write a function or type once and have it work correctly with any type, without giving up compile-time type safety:

Swift
func firstElement<T>(of array: [T]) -> T? {
    array.first
}

print(firstElement(of: [1, 2, 3]))          // Optional(1)
print(firstElement(of: ["a", "b", "c"]))     // Optional("a")
Swift
struct Stack<Element> {
    private var items: [Element] = []

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        items.popLast()
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop())   // Optional(2)

Generic constraints let you require a type parameter conform to a protocol, so you can rely on that protocol's behavior inside the generic function:

Swift
func largest<T: Comparable>(_ items: [T]) -> T? {
    items.max()   // .max() requires T to conform to Comparable
}

print(largest([3, 7, 2]))          // Optional(7)
print(largest(["banana", "apple"])) // Optional("banana")

Error handling — throws, try, do-catch

Swift models recoverable errors with the Error protocol and a dedicated throws/try/do-catch mechanism, distinct from Optionals:

Swift
enum ValidationError: Error {
    case tooShort
    case tooLong
}

func validate(username: String) throws -> String {
    if username.count < 3 {
        throw ValidationError.tooShort
    }
    if username.count > 20 {
        throw ValidationError.tooLong
    }
    return username
}

do {
    let name = try validate(username: "ab")
    print("Valid: \(name)")
} catch ValidationError.tooShort {
    print("Username is too short")
} catch ValidationError.tooLong {
    print("Username is too long")
} catch {
    print("Unexpected error: \(error)")
}

try? converts a throwing call's result into an Optional (nil on failure, discarding the specific error) — useful when you only care whether it succeeded:

Swift
let result = try? validate(username: "ab")
print(result ?? "invalid")   // invalid

try! force-attempts the call, crashing at runtime if it throws — used exactly as sparingly as force-unwrapping an optional, and for the same reason.

ARC and retain cycles, briefly

Classes in Swift are memory-managed with Automatic Reference Counting (ARC): every class instance keeps an internal count of how many references point to it, and is deallocated automatically the moment that count hits zero. Unlike a garbage collector, this happens deterministically and immediately, not on some unpredictable future scan — but it comes with one real hazard: retain cycles.

A retain cycle happens when two instances hold strong references to each other, so each one's count never reaches zero and neither is ever deallocated:

Swift
class Owner {
    var pet: Pet?
}

class Pet {
    var owner: Owner?   // strong reference back to Owner — creates a cycle
}

var owner: Owner? = Owner()
var pet: Pet? = Pet()
owner?.pet = pet
pet?.owner = owner   // owner and pet now strongly reference each other

owner = nil
pet = nil
// Neither Owner nor Pet is ever deallocated — each still holds a strong
// reference to the other, even though nothing outside references them anymore.

The fix is to mark one side of a two-way relationship weak (or unowned), so it doesn't contribute to the reference count:

Swift
class Pet {
    weak var owner: Owner?   // weak — doesn't keep Owner alive on its own
}

Use weak (always an Optional, since the referenced instance can be deallocated out from under it, becoming nil automatically) for relationships where the reference might legitimately become invalid. Use unowned only when you're certain the reference will never outlive the instance it points to — an incorrect unowned crashes at runtime instead of safely becoming nil.

Common mistakes

  • Creating a parent-child relationship with strong references on both sides, silently leaking memory — always ask "which side of this two-way relationship should be weak?"
  • Forgetting that closures capture surrounding variables (including self) strongly by default — a closure stored as a class property that captures self can itself create a retain cycle, usually fixed with a [weak self] capture list.
  • Reaching for try! or try? reflexively instead of a proper do-catch when the specific error actually matters to how the program should respond.

Interview questions

Q: What is ARC, and how does it differ from a tracing garbage collector? ARC (Automatic Reference Counting) tracks how many strong references point to each class instance and deallocates it the instant that count reaches zero — deterministic and immediate. A tracing garbage collector instead periodically scans memory for unreachable objects, which is less predictable in timing (can introduce pauses) but doesn't have ARC's specific weakness: reference cycles.

Q: What is a retain cycle, and how do you prevent one? It happens when two class instances hold strong references to each other, so neither's reference count ever reaches zero and neither is ever deallocated, even after nothing else references them — a memory leak. It's prevented by marking one side of the relationship weak (or unowned, when you're certain the reference will always outlive it), so that side doesn't keep the count above zero on its own.

Q: What's the difference between try?, try!, and a do-catch block? try? converts a throwing call into an Optional, returning nil on failure and discarding the specific error. try! force-attempts the call and crashes at runtime if it throws. A do-catch block lets you inspect and handle the specific error thrown, which is the right choice whenever the caller needs to react differently depending on what went wrong.