Part of Swift with Copilot

Swift: Pass a closure to a function

Sandy LaneSandy Lane

Video: Swift: Pass a closure to a function by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

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 behind map, 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 patternsmap, 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.