Swift: Pass a closure to a function
Video: Swift: Pass a closure to a function by Taught by Celeste AI - AI Coding Coach
Swift with Copilot: Pass a Closure to a Function
A function that takes a closure has a parameter typed as
(args) -> Return. The caller passes a closure literal — usually with trailing closure syntax. The pattern behindmap,filter,reduce,forEach, and almost every callback in Swift. Series finale.
You've seen arr.map { $0 * 2 } a dozen times by now. This episode is the underlying mechanic: how to write a function that takes a closure, and how to call it.
The Copilot prompt
// Define a function that takes a closure and applies it to two numbers
Copilot completes:
func applyOperation(a: Int, b: Int, operation: (Int, Int) -> Int) -> Int {
return operation(a, b)
}
let sum = applyOperation(a: 5, b: 3, operation: +)
let product = applyOperation(a: 5, b: 3) { $0 * $1 }
print(sum) // 8
print(product) // 15
Walkthrough
Function signature
func applyOperation(a: Int, b: Int, operation: (Int, Int) -> Int) -> Int {
return operation(a, b)
}
a: Int, b: Int— regular parameters.operation: (Int, Int) -> Int— a parameter typed as a function/closure.- Returns
Int.
The (Int, Int) -> Int syntax says "any callable that takes two Ints and returns an Int" — a function, a closure, or even an operator like +.
Calling: passing a function
applyOperation(a: 5, b: 3, operation: +)
+ is an operator. In Swift, operators are functions. + for two Ints has type (Int, Int) -> Int — exactly what operation: wants. Pass directly.
Calling: passing a closure
applyOperation(a: 5, b: 3, operation: { $0 * $1 })
A closure literal { $0 * $1 } is also (Int, Int) -> Int.
Trailing closure syntax
When the last parameter is a closure, you can move it outside the parens:
applyOperation(a: 5, b: 3) { $0 * $1 }
Cleaner. For multi-line closures, much cleaner:
applyOperation(a: 5, b: 3) { x, y in
let result = x * y
print("multiplying \(x) and \(y): \(result)")
return result
}
Higher-order functions
A "higher-order function" is one that takes or returns another function. map, filter, reduce are the classic examples.
Custom map-like function
func doubled<T>(_ array: [T], using transform: (T) -> T) -> [T] {
array.map(transform)
}
doubled([1, 2, 3]) { $0 * 2 } // [2, 4, 6]
Generic over T; the closure does whatever transformation.
Custom filter
func keep<T>(_ array: [T], where keep: (T) -> Bool) -> [T] {
array.filter(keep)
}
keep([1, 2, 3, 4]) { $0.isMultiple(of: 2) } // [2, 4]
Returning a closure
func multiplier(by factor: Int) -> (Int) -> Int {
return { value in value * factor }
}
let triple = multiplier(by: 3)
print(triple(10)) // 30
print(triple(5)) // 15
multiplier(by:) returns a closure. The closure captures factor from the outer scope.
This is currying / partial application — making a generic function specific to a parameter.
Callbacks
func fetchUser(completion: (User?) -> Void) {
// ... fetch ...
completion(user)
}
fetchUser { user in
if let user = user {
print("Got: \(user.name)")
} else {
print("Failed")
}
}
Pre-async/await Swift used closures for async results. Still common in older codebases.
@escaping closures
If the closure outlives the function (stored, run async), mark @escaping:
var pending: [() -> Void] = []
func register(_ handler: @escaping () -> Void) {
pending.append(handler)
}
register { print("hi") }
// Later:
pending.first?() // "hi"
Without @escaping, the closure must run before the function returns. The default is non-escaping (faster: no retain cycle risk).
@autoclosure
func log(_ message: @autoclosure () -> String) {
if isVerbose {
print(message())
}
}
log("Expensive: \(slow_computation())")
@autoclosure wraps the argument in a closure automatically. Useful for lazy evaluation — the expensive computation only runs if isVerbose.
assert(_:_:) and ?? use @autoclosure internally.
Multiple trailing closures
For functions with several closure parameters (Swift 5.3+):
func loadData(
fetch: () -> Data,
process: (Data) -> Result,
display: (Result) -> Void
) { ... }
loadData {
fetch()
} process: { data in
parse(data)
} display: { result in
show(result)
}
The first closure is unlabeled; subsequent ones use parameter names. Useful for SwiftUI-style chaining.
Real-world patterns
Sort with closure
let people = [...]
let byAge = people.sorted { $0.age < $1.age }
Animation completion
UIView.animate(withDuration: 0.3) {
view.alpha = 0
} completion: { _ in
view.removeFromSuperview()
}
Async work (older Swift)
DispatchQueue.global().async {
let data = expensive()
DispatchQueue.main.async {
self.update(with: data)
}
}
Modern Swift uses async/await for this — but closures are still everywhere.
Generic higher-order
func transform<Input, Output>(_ value: Input, using fn: (Input) -> Output) -> Output {
fn(value)
}
transform(5) { $0 * 2 } // 10 (Int)
transform("hi") { $0.uppercased() } // "HI" (String)
The closure type drives the generic — Input and Output are inferred from how the closure transforms.
A complete example: pipeline
func pipeline<T>(_ initial: T, _ steps: [(T) -> T]) -> T {
steps.reduce(initial) { value, step in step(value) }
}
let result = pipeline(10, [
{ $0 + 5 },
{ $0 * 2 },
{ $0 - 1 },
])
// 10 → 15 → 30 → 29
print(result) // 29
A list of closures, applied in order. Composable transformations.
Common stumbles
Closure type without parens. Even one parameter needs parens: (Int) -> Int, not Int -> Int.
Forgetting @escaping. Compiler will tell you. Add it when storing or running async.
@escaping and self. Risk of retain cycles. Use [weak self] capture lists.
Trailing closure ambiguity. When multiple methods could match, sometimes Swift can't disambiguate trailing closures. Use parens.
Closure vs function reference. arr.map(double) passes the function. arr.map { double($0) } wraps in a closure. Both work; the first is shorter.
Forgot return. Single-expression closures don't need it; multi-expression do.
The series ends here
Across 20 episodes, we've covered:
- Setup — Swift in VS Code, Copilot, running scripts.
- Arrays and ranges — random lists, sorting, transforming with map.
- Strings and dates — DateFormatter, Calendar, leap years, weekdays.
- Functional patterns —
map,filter,reduce,compactMap. - Functions and closures — declaration, calling, capturing, escaping.
- Standard idioms — dedup, merge, swap, shuffle.
Enough to read most Swift code, write everyday transformations, and use Copilot productively in VS Code.
Next steps
Pick a project. Some ideas:
- A SwiftUI app. Even a small one — task list, weather, notes. Use Xcode for that.
- A command-line tool with
swift package init --type executable. Arg parsing, file I/O, deploy as a Homebrew formula. - Server-side with Vapor. Routes, models, JSON.
- Contribute to swift-algorithms or swift-collections. Apple's open-source packages — well-maintained, friendly to first-time contributors.
The language rewards depth. Once you internalize closures, generics, and protocols, Swift starts to feel natural.
Recap
(Args) -> Return is the closure type. Pass closures or function references — operators like + work. Trailing closure syntax for last-arg closures: f(x) { ... }. @escaping for stored or async closures. @autoclosure for lazy eval. Multiple trailing closures (Swift 5.3+) for SwiftUI-style APIs. The series of episodes is your foundation.
Series complete. Build something with Swift — the only way to truly learn it.