Skip to content

Functions | Swift - Wyatt's Notes

Functions are self-contained blocks of code that perform a specific task. Swift functions are first-class citizens: they can be assigned to variables, passed as arguments, and returned from other functions.

func greet(name: String) -> String {
return "Hello, \(name)!"
}
print(greet(name: "Alice")) // Hello, Alice!
// Functions without parameters
func sayHello() {
print("Hello, World!")
}
// Functions without return value
func log(_ message: String) {
print("[LOG] \(message)")
}
// Implicit return for single-expression functions
func double(_ x: Int) -> Int { x * 2 }
// Argument label + parameter name
func greet(person name: String) -> String {
return "Hello, \(name)!"
}
greet(person: "Alice") // Argument label is "person'
// Omitting argument label with _
func square(_ number: Int) -> Int {
return number * number
}
square(5) // No argument label needed
// Default parameter values
func power(_ base: Int, _ exponent: Int = 2) -> Int {
var result = 1
for _ in 0..<exponent {
result *= base
}
return result
}
power(3) // 9 (exponent defaults to 2)
power(3, 3) // 27
// Variadic parameters
func average(_ numbers: Double...) -> Double {
guard !numbers.isEmpty else { return 0 }
let sum = numbers.reduce(0, +)
return sum / Double(numbers.count)
}
average(1, 2, 3, 4, 5) // 3.0
// Inout parameters -- modify the caller's variable
func swapValues(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 10, y = 20
swapValues(&x, &y)
print("x: \(x), y: \(y)") // x: 20, y: 10
// Multiple return values with tuples
func minMax(array: [Int]) -> (min: Int, max: Int)? {
guard let first = array.first else { return nil }
var currentMin = first
var currentMax = first
for value in array {
if value < currentMin { currentMin = value }
if value > currentMax { currentMax = value }
}
return (currentMin, currentMax)
}
if let bounds = minMax(array: [3, 7, 1, 9, 4]) {
print("Min: \(bounds.min), Max: \(bounds.max)")
}

Every function has a type, composed of its parameter types and return type.

func add(_ a: Int, _ b: Int) -> Int { a + b }
func multiply(_ a: Int, _ b: Int) -> Int { a * b }
// Function type: (Int, Int) -> Int
var operation: (Int, Int) -> Int = add
print(operation(3, 4)) // 7
operation = multiply
print(operation(3, 4)) // 12
// Function type as parameter
func apply(_ a: Int, _ b: Int, _ f: (Int, Int) -> Int) -> Int {
return f(a, b)
}
apply(3, 4, add) // 7
apply(3, 4, multiply) // 12
apply(3, 4, { $0 - $1 }) // -1
// Function type as return type
func stepped(increment: Bool) -> (Int) -> Int {
if increment {
return { $0 + 1 }
} else {
return { $0 - 1 }
}
}
let stepUp = stepped(increment: true)
print(stepUp(5)) // 6
func selectOperation(_ mode: String) -> (Double, Double) -> Double {
func add(_ a: Double, _ b: Double) -> Double { a + b }
func subtract(_ a: Double, _ b: Double) -> Double { a - b }
func multiply(_ a: Double, _ b: Double) -> Double { a * b }
switch mode {
case "add": return add
case "subtract": return subtract
case "multiply": return multiply
default: return add
}
}
let calc = selectOperation("add")
print(calc(3.0, 4.0)) // 7.0

Closures are self-contained blocks of code that can capture and store references to constants and variables from their surrounding context. Swift handles all memory management for captured variables automatically.

// Full syntax
let greetFull = { (name: String) -> String in
return "Hello, \(name)!"
}
// Inferring type from context
let names = ["Alice", "Bob", "Carol"]
let reversed = names.sorted(by: { (a: String, b: String) -> Bool in
return a > b
})
// Implicit returns from single-expression closures
let sorted = names.sorted(by: { a, b in a > b })
// Shorthand argument names ($0, $1, ...)
let shortest = names.sorted(by: { $0.count < $1.count })
// Operator methods as closures
let alphabetical = names.sorted(by: <)

When the last argument of a function is a closure, write it after the function call using trailing closure syntax.

func transform(_ values: [Int], using closure: (Int) -> Int) -> [Int] {
return values.map(closure)
}
// Trailing closure
let doubled = transform([1, 2, 3]) { $0 * 2 } // [2, 4, 6]
// Multiple trailing closures (Swift 5.3+)
func load(url: String, onSuccess: (Data) -> Void, onFailure: (Error) -> Void) {
// ...
}
load(url: "https://example.com") { data in
print("Success: \(data.count) bytes")
} onFailure: { error in
print("Failure: \(error)")
}
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
print(counter()) // 1
print(counter()) // 2
print(counter()) // 3
// 'count' is captured and persists across calls
func makeIncrementer(increment amount: Int) -> () -> Int {
var total = 0
return {
total += amount
return total
}
}
let incrementBy5 = makeIncrementer(increment: 5)
print(incrementBy5()) // 5
print(incrementBy5()) // 10
print(incrementBy5()) // 15

Control how values are captured by using [unowned self] or [weak self].

class NetworkManager {
var requestCount = 0
func fetchData(completion: @escaping () -> Void) {
// Without capture list: strong reference to self
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.requestCount += 1
completion()
}
// With capture list: weak reference (avoid retain cycle)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.requestCount += 1
completion()
}
}
}

A closure is non-escaping by default, meaning it is executed before the function returns. An escaping closure is stored or executed after the function returns, requiring the @escaping annotation.

// Non-escaping (default)
func perform(_ action: () -> Void) {
action() // Executed synchronously before return
}
// Escaping -- closure outlives the function
class EventStore {
var handlers: [() -> Void] = []
func subscribe(_ handler: @escaping () -> Void) {
handlers.append(handler) // Stored for later execution
}
func notify() {
handlers.forEach { $0() }
}
}
// Escaping with async work
func download(url: String, completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: url)!) { data, response, error in
if let data {
completion(.success(data))
} else if let error {
completion(.failure(error))
}
}.resume()
}
// Swift 6 requires Sendable for closures sent across concurrency boundaries
func submitTask(_ work: @escaping @Sendable () -> Void) {
Task.detached {
work()
}
}

An @autoclosure wraps an expression in a closure, deferring evaluation until the closure is called.

// Evaluates condition lazily
func assert(_ condition: @autoclosure () -> Bool, _ message: String) {
if !condition() {
print("Assertion failed: \(message)")
}
}
var debugMode = false
assert(debugMode, "Debug mode should be on")
// The expression `debugMode` is only evaluated inside assert()
// @autoclosure + @escaping
func collect(operations: inout [@escaping () -> Void], _ op: @autoclosure @escaping () -> Void) {
operations.append(op)
}

Swift provides several higher-order functions on collections.

let prices = [10.0, 20.0, 30.0]
let withTax = prices.map { $0 * 1.2 }
// [12.0, 24.0, 36.0]
// Map with index
let indexed = prices.enumerated().map { (index, price) in
"\(index + 1). $\(price)"
}
// ["1. $10.0", "2. $20.0", "3. $30.0"]
// flatMap for flattening nested arrays
let nested = [[1, 2], [3], [4, 5, 6]]
let flat = nested.flatMap { $0 }
// [1, 2, 3, 4, 5, 6]
// flatMap for filtering nil from optionals
let inputs: [String?] = ["42", nil, "hello", "7", nil]
let numbers = inputs.compactMap { $0.flatMap(Int.init) }
// [42, 7]
// compactMap -- filter out nil, unwrap non-nil
let possibleNumbers = ["1", "two", "3", "four", "5"]
let validNumbers = possibleNumbers.compactMap { Int($0) }
// [1, 3, 5]
let scores = [45, 82, 67, 91, 55, 78, 93, 60]
let passing = scores.filter { $0 >= 60 }
// [82, 67, 91, 78, 93, 60]
let highScorers = scores.filter { $0 >= 80 }.sorted(by: >)
// [93, 91, 82]
let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, +) // 15
let product = numbers.reduce(1, *) // 120
let joined = ["Hello", "World"].reduce("", { $0 + " " + $1 })
// " Hello World"
// reduce(into:) for efficiency
let grouped: [Character: [Int]] = [1, 2, 3, 4, 5].reduce(into: [:]) { result, number in
let key = number % 2 == 0 ? "e" : "o"
result[key, default: []].append(number)
}
// ["o": [1, 3, 5], "e": [2, 4]]
let words = ["apple", "banana", "cherry", "avocado"]
words.contains { $0.hasPrefix("a") } // true
words.first { $0.count > 5 } // "banana"
words.allSatisfy { $0.count > 2 } // true
let names = ["Alice", "Bob", "Carol"]
names.forEach { print("Hello, \($0)") }
// Note: cannot use break or continue in forEach
// Use for-in if you need control flow
let students = [
(name: "Alice", score: 92),
(name: "Bob", score: 78),
(name: "Carol", score: 95),
(name: "Dave", score: 82),
(name: "Eve", score: 61)
]
let honours = students
.filter { $0.score >= 80 }
.sorted { $0.score > $1.score }
.map { "\($0.name): \($0.score)" }
// ["Carol: 95", "Alice: 92", "Dave: 82"]

Property wrappers encapsulate get/set logic in a reusable wrapper type. SwiftUI relies heavily on property wrappers.

@propertyWrapper
struct Capitalized {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.capitalized }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
struct UserProfile {
@Capitalized var firstName: String
@Capitalized var lastName: String
}
var profile = UserProfile(firstName: "alice", lastName: "smith")
print(profile.firstName) // Alice
print(profile.lastName) // Smith
@propertyWrapper
struct Clamped<Value: Comparable> {
var wrappedValue: Value {
didSet { wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) }
}
let range: ClosedRange<Value>
var projectedValue: ClosedRange<Value> { range }
init(wrappedValue: Value, range: ClosedRange<Value>) {
self.range = range
self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
struct GameSettings {
@Clamped(range: 0...100) var volume: Int = 50
}
var settings = GameSettings()
print(settings.volume) // 50
settings.volume = 150
print(settings.volume) // 100 (clamped)
print(settings.$volume) // 0...100 (projected value)
import SwiftUI
struct CounterView: View {
// @State: Local state, value type, view-owned
@State private var count = 0
// @Binding: Two-way binding to a parent's state
// Used in child views
// @ObservedObject: Reference type conforming to ObservableObject
@ObservedObject var viewModel = GameViewModel()
// @StateObject: Owns and creates the ObservableObject
@StateObject var manager = DataManager()
// @EnvironmentObject: Injected from parent hierarchy
@EnvironmentObject var appSettings: AppSettings
// @Environment: Read system/environment values
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
// @FetchRequest: Core Data query
// @ScaledMetric: Dynamic type scaling
// @FocusState: Keyboard focus management
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") { count += 1 }
}
}
}
// Binding example
struct ParentView: View {
@State private var isOn = false
var body: some View {
ToggleView(isOn: $isOn) // Pass binding
}
}
struct ToggleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Feature", isOn: $isOn)
}
}

Key paths provide type-safe references to properties.

struct Person {
let name: String
var age: Int
}
let nameKeyPath = \Person.name
let ageKeyPath = \Person.age
let alice = Person(name: "Alice", age: 30)
print(alice[keyPath: nameKeyPath]) // Alice
// Key paths with arrays
let people = [
Person(name: "Alice", age: 30),
Person(name: "Bob", age: 25),
Person(name: "Carol", age: 35)
]
let names = people.map(\.name) // ["Alice", "Bob", "Carol"]
let sorted = people.sorted(by: \.age) // [Bob(25), Alice(30), Carol(35)]
// Key paths in sorting and filtering
let adults = people.filter { $0[keyPath: \.age] >= 30 }
// [Alice(30), Carol(35)]
enum CompassDirection {
case north, south, east, west
}
var direction = CompassDirection.north
direction = .south // Shorthand when type is known
switch direction {
case .north: print("Heading north")
case .south: print("Heading south")
case .east: print("Heading east")
case .west: print("Heading west")
}
enum NetworkResponse {
case success(data: Data, statusCode: Int)
case failure(error: Error)
case redirect(to: URL)
}
func handle(response: NetworkResponse) {
switch response {
case .success(let data, let code):
print("Success (\(code)): \(data.count) bytes")
case .failure(let error):
print("Error: \(error.localizedDescription)")
case .redirect(let url):
print("Redirect to: \(url)")
}
}
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
let earthOrder = Planet.earth.rawValue // 3
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
let method = HTTPMethod.post.rawValue // "POST"
// Initialising from raw value
if let planet = Planet(rawValue: 3) {
print(planet) // earth
}
enum Operation {
static func add(_ a: Double, _ b: Double) -> Double { a + b }
static func subtract(_ a: Double, _ b: Double) -> Double { a - b }
static func multiply(_ a: Double, _ b: Double) -> Double { a * b }
static func divide(_ a: Double, _ b: Double) -> Double { a / b }
}
let compute: (Double, Double) -> Double = Operation.add
print(compute(3.0, 4.0)) // 7.0
struct Matrix {
let rows: Int, cols: Int
var grid: [Double]
init(rows: Int, cols: Int) {
self.rows = rows
self.cols = cols
self.grid = Array(repeating: 0, count: rows * cols)
}
subscript(row: Int, col: Int) -> Double {
get { grid[row * cols + col] }
set { grid[row * cols + col] = newValue }
}
}
var m = Matrix(rows: 3, cols: 3)
m[0, 1] = 5.0
print(m[0, 1]) // 5.0

Closures in Swift are like captured moments. A closure remembers the variables from the context where it was created, even after that context is gone. This is like a photograph that preserves not just the subject but the entire scene. The closure carries that scene with it wherever it goes.

Trailing closure syntax is like putting the most important instruction last. When the last argument to a function is a closure, Swift lets you write it outside the parentheses. This makes the code read more logically, like saying I want to do this, and here is how.

Example 1: Curried Function for Configuration

Section titled “Example 1: Curried Function for Configuration”

Problem: Create a curried function that builds a URL request from a base URL, path, and headers. Each step should be independently reusable.

func makeRequest(base: String) -> (String) -> ([(String, String)]) -> URLRequest {
return { path in
return { headers in
let url = URL(string: base + path)!
var request = URLRequest(url: url)
for (key, value) in headers {
request.addValue(value, forHTTPHeaderField: key)
}
return request
}
}
}
let apiRequest = makeRequest(base: "https://api.example.com")
let usersRequest = apiRequest("/users")([("Authorization", "Bearer token123")])
print(usersRequest.url?.absoluteString ?? "")
// https://api.example.com/users

Explanation: Currying transforms a multi-parameter function into a chain of single-parameter functions. Each intermediate function can be stored and reused independently. This pattern is useful for building configurable factories where some parameters are fixed early and others are provided later.


Example 2: Closure-Based Retry with Exponential Backoff

Section titled “Example 2: Closure-Based Retry with Exponential Backoff”

Problem: Write a higher-order function that retries an async operation with exponential backoff, accepting a custom retry handler via a closure parameter.

func withRetry<T>(
maxAttempts: Int,
initialDelay: TimeInterval,
operation: @escaping () async throws -> T,
onRetry: ((Int, Error) -> Void)? = nil
) async throws -> T {
var lastError: Error?
var delay = initialDelay
for attempt in 1...maxAttempts {
do {
return try await operation()
} catch {
lastError = error
onRetry?(attempt, error)
if attempt < maxAttempts {
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
delay *= 2
}
}
}
throw lastError!
}
// Usage
let result = try await withRetry(maxAttempts: 3, initialDelay: 1.0, operation: {
try await fetchUserProfile(id: 42)
}, onRetry: { attempt, error in
print("Attempt \(attempt) failed: \(error)")
})

Explanation: The function accepts operation as an @escaping closure because it is called asynchronously across Task.sleep boundaries. The optional onRetry closure provides a hook for logging or metrics without affecting retry logic. Exponential backoff doubles the delay after each failure.


Example 3: Type-Safe Event Emitter with Closures

Section titled “Example 3: Type-Safe Event Emitter with Closures”

Problem: Implement an event emitter that uses closures as handlers, supporting typed events and allowing handlers to be registered and removed.

class EventEmitter<EventType: Hashable> {
private var handlers: [EventType: [String: (Any) -> Void]] = [:]
private var counter = 0
func on<T>(_ event: EventType, handler: @escaping (T) -> Void) -> String {
let id = "handler_\(counter)"
counter += 1
handlers[event, default: :][id] = { value in
guard let typed = value as? T else { return }
handler(typed)
}
return id
}
func off(_ event: EventType, id: String) {
handlers[event]?.removeValue(forKey: id)
}
func emit<T>(_ event: EventType, _ value: T) {
handlers[event]?.values.forEach { $0(value) }
}
}
// Usage
enum AppEvent {
case login, logout, settingsChanged
}
let emitter = EventEmitter<String>()
let loginId = emitter.on("login") { (name: String) in
print("Welcome, \(name)!")
}
emitter.on("login") { (name: String) in
print("Audit: \(name) logged in")
}
emitter.emit("login", "Alice")
// Welcome, Alice!
// Audit: Alice logged in
emitter.off("login", id: loginId)
emitter.emit("login", "Bob")
// Audit: Bob logged in

Explanation: The generic EventType parameter allows the emitter to be keyed by any hashable type (strings, enums, etc.). Each handler is stored with a unique string ID for later removal. The internal storage uses (Any) -> Void with type casting, providing type safety at the public API boundary while maintaining flexibility internally.

flowchart TD
    A[1_Functions] --> B[Key Concepts]
    A --> C[Core Principles]
    A --> D[Practical Applications]
    B --> E[Fundamental definitions]
    C --> F[Design patterns]
    D --> G[Real-world usage]

Swift functions are first-class values that can be stored, passed, and returned. Closures provide inline function definitions with shorthand syntax. Escaping closures handle asynchronous work, while property wrappers encapsulate storage logic. Higher-order functions (map, filter, reduce) enable concise, expressive data transformations.

Forgetting that closures capture variables by reference. Closures in Swift capture variables by reference, not by value. If a closure modifies a captured variable, the change persists after the closure executes. This can cause unexpected side effects. Use capture lists ([x]) to capture values explicitly when needed.

Confusing trailing closure syntax with regular function calls. When the last argument to a function is a closure, you can use trailing closure syntax: func { body } instead of func({ body }). However, this only works for the last closure parameter. If there are multiple closure parameters, only the last one can use trailing syntax.

Not distinguishing between escaping and non-escaping closures. By default, closure parameters are non-escaping, meaning they execute before the function returns. Mark closures as @escaping when they are stored or executed after the function returns (e.g., in async callbacks). Forgetting @escaping causes compilation errors for stored closures.

  • Variables and Types - How closures capture variables and how optionals affect function signatures
  • Error Handling - How throwing functions extend the function type system
  • Classes and Structs - How methods, initializers, and deinitializers structure object behavior