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
- • Write closures with the { params -> Type in … } form
- • Use trailing closure syntax for cleaner calls
- • Shrink closures with shorthand $0, $1 and inferred returns
- • Understand how closures capture surrounding values
- • Tell escaping from non-escaping closures
- • Know when self must be written explicitly
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)) // 202️⃣ 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 callsYour 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)) // 14Pro Tips
- 💡 Use trailing closures for the final argument — they read best for multi-line bodies.
- 💡 Reach for $0 sparingly. Shorthand is great for tiny closures; name parameters when logic grows.
- 💡 Mark stored callbacks @escaping. Completion handlers almost always escape.
- 💡 Watch retain cycles. In escaping closures capturing self, consider [weak self].
Common Errors (and the fix)
- "Escaping closure captures non-escaping parameter" — you stored a closure that wasn't marked @escaping. Add the attribute.
- "Reference to property 'x' in closure requires explicit use of 'self'" — in an escaping closure, write self.x.
- "Contextual closure type expects N arguments" — your closure's parameter count doesn't match. Match the expected signature.
- "Missing return in closure" — a multi-statement closure needs an explicit return.
📋 Quick Reference
| Task | Code | Note |
|---|---|---|
| Full closure | { (a: Int) -> Int in a*2 } | signature + in |
| Shorthand | { $0 < $1 } | positional args |
| Trailing | run { … } | after the parens |
| No params | () -> Void | empty closure type |
| Escaping | @escaping () -> Void | stored 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!
- ✅ Closures: { params -> Type in body }
- ✅ Trailing closures move the last closure outside the parentheses
- ✅ Shorthand $0/$1 and inferred returns trim the noise
- ✅ Closures capture and keep surrounding values alive
- ✅ @escaping closures run after the function returns
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
- Previous: Methods & Initializers
- Next: map, filter & reduce — Transform collections functionally with map, filter, reduce and compactMap
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.