Closures in Depth

Reviewed & published by Brayan K

Closures are blocks of code you can pass around like values. This lesson covers full and shorthand syntax, trailing closures, capturing values, and the difference between escaping and non-escaping closures — the foundation of callbacks and functional Swift.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Closure Syntax

The full closure form is { (params) -> ReturnType in body }. The in keyword separates the signature from the body. A closure can take no parameters and return nothing — its type is then () -> Void.

// A closure is a block of code you can store in a constant and call later.
// Full form:  { (parameters) -> ReturnType in  body }
let add = { (a: Int, b: Int) -> Int in
    return a + b
}
print(add(2, 3))   // 5

// A closure with no parameters and no return value:
let sayHi = { print("Hi!") }
sayHi()            // Hi!

// You can declare the type separately and assign a closure to it:
let multiply: (Int, Int) -> Int = { a, b in a * b }
print(multiply(4, 5))   // 20

2️⃣ Trailing Closures

When a closure is the last argument to a function, you can write it after the parentheses. This trailing-closure style is everywhere in Swift and SwiftUI because it reads so cleanly.

// When a function's LAST argument is a closure, you can write it as a
// 'trailing closure' AFTER the parentheses — it reads much more cleanly.
func repeatTask(times: Int, task: () -> Void) {
    for _ in 1...times { task() }
}

// Without trailing closure:
repeatTask(times: 2, task: { print("tick") })

// With trailing closure (preferred):
repeatTask(times: 2) {
    print("tock")
}

3️⃣ Shorthand: $0, Inferred Types & Returns

Swift lets you drop a lot of ceremony. Parameter types can be inferred, a single-expression body needs no return, and $0/$1 name the arguments positionally.

// Swift can shrink closures dramatically using shorthand argument names
// ($0, $1, ...) and inferred returns.
let names = ["Charlie", "Alice", "Bob"]

// Full closure passed to sorted(by:):
let sorted1 = names.sorted(by: { (a: String, b: String) -> Bool in
    return a < b
})

// Inferred types + implicit return:
let sorted2 = names.sorted(by: { a, b in a < b })

// Shorthand arguments $0 and $1:
let sorted3 = names.sorted(by: { $0 < $1 })

print(sorted1)   // ["Alice", "Bob", "Charlie"]
print(sorted3)   // ["Alice", "Bob", "Charlie"]

4️⃣ Capturing Values (and Escaping)

A closure captures variables from the scope where it's defined and keeps them alive. A closure that's stored and called after the function returns is escaping — you mark its parameter @escaping, and using self inside it must be explicit.

// A closure CAPTURES variables from the scope where it's created and keeps
// using them — even after that scope is gone.
func makeCounter() -> () -> Int {
    var count = 0
    // This closure captures 'count' by reference and increments it each call.
    let increment: () -> Int = {
        count += 1
        return count
    }
    return increment
}

let counter = makeCounter()
print(counter())   // 1
print(counter())   // 2
print(counter())   // 3   — state is remembered between calls

Your turn. Fill in the blanks to use shorthand arguments and the in keyword.

// 🎯 YOUR TURN — fill in the blanks, then run it in SwiftFiddle.
let numbers = [5, 2, 8, 1]

// 1) Sort descending using SHORTHAND arguments. Replace ___ with $0 and $1.
let desc = numbers.sorted(by: { ___ > ___ })   // 👉 use $0 > $1
print(desc)   // [8, 5, 2, 1]

// 2) Write a closure that doubles a number. Replace ___ with the 'in' keyword.
let double = { (n: Int) -> Int ___    // 👉 replace ___ with 'in'
    return n * 2
}
print(double(7))   // 14

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNote
Full closure{ (a: Int) -> Int in a*2 }signature + in
Shorthand{ $0 < $1 }positional args
Trailingrun { … }after the parens
No params() -> Voidempty closure type
Escaping@escaping () -> Voidstored for later

Mini-Challenge: applyTwice

No blanks this time — just a brief and an outline. Build it, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: a configurable greeter
// 1. Write a function applyTwice(to value: Int, using transform: (Int) -> Int)
//    that returns transform(transform(value)).
// 2. Call it with value 3 and a trailing closure that adds 10.
// 3. Print the result.
//
// ✅ Expected output:
//    23

// your code here

🎉 Lesson Complete!

Practice quiz

What is a closure in Swift?

  • A class that can't be subclassed
  • A type of optional
  • A self-contained block of code that can be passed around
  • A protocol requirement

Answer: A self-contained block of code that can be passed around. A closure is a self-contained block of functionality that can be stored, passed, and called later.

What keyword separates a closure's parameters from its body?

  • in
  • return
  • do
  • then

Answer: in. The 'in' keyword separates the parameter/return list from the closure body.

What is a trailing closure?

  • A closure stored in a variable
  • A closure that returns nothing
  • A closure with no parameters
  • A closure written after the call's parentheses when it's the last argument

Answer: A closure written after the call's parentheses when it's the last argument. When a closure is the final argument, you can move it outside the parentheses as a trailing closure.

What does the shorthand argument $0 refer to?

  • The return value
  • The first parameter of the closure
  • The closure itself
  • An optional binding

Answer: The first parameter of the closure. $0 is shorthand for the first parameter, $1 the second, and so on.

What does it mean to 'capture' a value in a closure?

  • To keep a reference to a variable from the surrounding scope
  • To copy it into a struct
  • To make it constant
  • To delete it after use

Answer: To keep a reference to a variable from the surrounding scope. A closure captures variables/constants from its surrounding context and can keep using them.

What is an escaping closure?

  • A closure that throws errors
  • A closure that can't capture values
  • A closure stored and called AFTER the function returns
  • A closure with no body

Answer: A closure stored and called AFTER the function returns. An @escaping closure outlives the function call — it's stored and invoked later, e.g. in a completion handler.

By default, are closure parameters to a function escaping or non-escaping?

  • Escaping
  • Non-escaping
  • Always escaping for async
  • Neither — it must be specified

Answer: Non-escaping. Closure parameters are non-escaping by default; you opt in to escaping with @escaping.

When can you omit a closure's 'return' keyword?

  • Never
  • Only for trailing closures
  • Only when it returns Void
  • When the closure body is a single expression

Answer: When the closure body is a single expression. For a single-expression closure, Swift infers the return, so you can omit 'return'.

Which is a valid empty closure type that takes no parameters and returns nothing?

  • Void -> Int
  • () -> ()
  • (Int)
  • { }

Answer: () -> (). () -> () (or () -> Void) is a closure taking no arguments and returning nothing.

Why might @escaping closures require you to write self explicitly?

  • For performance
  • Because self is optional
  • To make capturing of self visible and avoid accidental retain cycles
  • It is never required

Answer: To make capturing of self visible and avoid accidental retain cycles. Escaping closures that use self require explicit self, making the strong capture (and possible retain cycle) obvious.

Continue this course

Frequently asked questions

What exactly is a closure in Swift?

A closure is a self-contained block of code that you can store in a variable, pass as an argument, and call later. Functions are actually a special, named case of closures. The general syntax is { (parameters) -> ReturnType in body }.

What does $0 mean in a Swift closure?

$0, $1, $2 and so on are shorthand argument names. When Swift can infer the parameter types, you can skip naming them and refer to them positionally: $0 is the first argument, $1 the second. For example { $0 < $1 } compares the first two arguments.

What is the difference between an escaping and a non-escaping closure?

A non-escaping closure (the default) is guaranteed to run before the function returns. An @escaping closure can be stored and called later — after the function has already returned — such as a network completion handler. You must mark such parameters @escaping, and using self inside them must be explicit.

What is a trailing closure and why is it preferred?

When a closure is the final argument to a function, you can write it outside the call's parentheses as a trailing closure. This reads more naturally, especially for multi-line closures and DSL-style APIs. If the closure is the only argument, you can even drop the parentheses entirely.

How does capturing values work in a closure?

A closure captures the variables and constants from its surrounding scope by reference, keeping them alive as long as the closure exists. This lets a returned closure remember and mutate state between calls — the basis of patterns like counters and memoization. Be mindful of retain cycles when capturing self in escaping closures.