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.
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:
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:
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):
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:
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")
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:
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:
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:
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:
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:
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 capturesselfcan itself create a retain cycle, usually fixed with a[weak self]capture list. - Reaching for
try!ortry?reflexively instead of a properdo-catchwhen 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.