Closures
A closure is a block of code you can store in a variable or pass to a function, like a function without a name. You met one already in scores.count(where: { $0 >= 80 }). Closures are everywhere in Swift: sorting and filtering collections, reacting to a button tap in SwiftUI, running code after a download finishes.
In this lesson you will write closures in full and then in shorter forms, use the most important collection methods that take closures, and learn how a closure can capture and remember values from around it.
From full syntax to shorthand
A closure expression is written in braces: parameters and return type, then the keyword in, then the body. Because Swift can usually infer the types, you can shorten it step by step.
let marks = [67, 91, 45, 88]
let full = marks.sorted(by: { (a: Int, b: Int) -> Bool in
return a > b
})
let inferred = marks.sorted(by: { a, b in a > b })
let shorthand = marks.sorted { $0 > $1 }
let operatorOnly = marks.sorted(by: >)
print(full)
print(full == inferred, inferred == shorthand, shorthand == operatorOnly)[91, 88, 67, 45] true true true
All four lines do the same thing:
- Full form: parameter names, types and return type are written out.
- Inferred types:
sorted(by:)expects(Int, Int) -> Bool, so the types andreturncan go. - Shorthand names:
$0and$1are the first and second arguments. When the closure is the last argument, it can move outside the brackets as a trailing closure. - Operator:
>is itself a function of type(Int, Int) -> Bool.
Use $0 for short, obvious closures. When the body is longer than one line, give parameters real names; your future self will thank you.
Closures with collection methods
These methods all take a closure and replace hand-written loops: map (transform), filter (keep some), reduce (combine), compactMap (transform and drop nil results), sorted(by:), first(where:), contains(where:) and allSatisfy.
let typed = ["120", "abc", "75", "", "300"]
let amounts = typed.compactMap { Int($0) }
print(amounts)
let orders = [(name: "Aarav", amount: 450), (name: "Priya", amount: 1200),
(name: "Kabir", amount: 800), (name: "Meera", amount: 1500)]
let bigSpenders = orders
.filter { $0.amount >= 800 }
.sorted { $0.amount > $1.amount }
.map { $0.name }
print(bigSpenders)
let revenue = orders.reduce(0) { total, order in total + order.amount }
print(revenue)
print(orders.allSatisfy { $0.amount > 100 })
print(orders.first { $0.amount > 1000 }?.name ?? "none")[120, 75, 300] ["Meera", "Priya", "Kabir"] 3950 true Priya
compactMap tried Int(...) on every string and kept only the successful numbers. The chain reads top to bottom like a recipe: keep orders of ₹800 or more, sort by amount, take the names. first(where:) (used here as a trailing closure) returns an optional, so ?.name and ?? handle the "nothing found" case.
Writing functions that take closures
A parameter can have a function type. The caller can then pass a closure, usually as a trailing closure.
func repeatTask(times: Int, task: (Int) -> Void) {
for round in 1...times {
task(round)
}
}
repeatTask(times: 3) { round in
print("Round \(round): practise 20 balls")
}Round 1: practise 20 balls Round 2: practise 20 balls Round 3: practise 20 balls
Void means "returns nothing". You will use exactly this style in SwiftUI, for example Button("Save") { save() }, where the trailing closure is the code to run on a tap. When a function takes several closures, Swift allows multiple trailing closures, each after the first one labelled, like Button { save() } label: { Text("Save") }.
Capturing values
A closure can use constants and variables from the place where it was created. It captures them, keeping them alive and remembering changes even after the original function has returned.
func makeTokenCounter(prefix: String) -> () -> String {
var next = 1
return {
let token = "\(prefix)-\(next)"
next += 1
return token
}
}
let opd = makeTokenCounter(prefix: "OPD")
let lab = makeTokenCounter(prefix: "LAB")
print(opd())
print(opd())
print(lab())
print(opd())OPD-1 OPD-2 LAB-1 OPD-3
Each call to makeTokenCounter creates a fresh next variable, and the returned closure keeps its own copy alive. That is why the hospital's OPD and lab counters do not interfere with each other. The return type () -> String means "a function with no parameters that returns a String".
Escaping closures
By default, a closure passed to a function must be used before the function returns. If the function stores the closure to call later, the parameter must be marked @escaping. The keyword tells readers, and the compiler, that the closure outlives the call.
func makeReminders() -> [() -> Void] {
var reminders: [() -> Void] = []
func add(_ action: @escaping () -> Void) {
reminders.append(action)
}
add { print("Send fee SMS to Isha") }
add { print("Print hall tickets") }
return reminders
}
let reminders = makeReminders()
print("Saved: \(reminders.count)")
for reminder in reminders {
reminder()
}Saved: 2 Send fee SMS to Isha Print hall tickets
The closures are stored in an array and run later, so add needs @escaping; without it the code does not compile. Escaping closures that capture an object can create memory leaks, which you will learn to prevent with [weak self] in Memory Management & ARC.
Common mistakes
- Overusing
$0in long closures. Name the parameters when the body is more than one simple line. - Using
mapwhen you needcompactMap.map { Int($0) }gives[Int?];compactMapgives[Int]. - Forgetting
@escaping. If you store a closure or call it later, mark the parameter@escaping. - Long chains with no line breaks. Put each
.filter,.sortedand.mapon its own line.
Key points
- A closure is an unnamed block of code written
{ params in body }that can be stored and passed around. - Type inference,
$0/$1and implicit returns make short closures very compact. - A closure passed as the last argument can be written as a trailing closure.
map,filter,reduce,compactMap,sorted(by:)andfirst(where:)replace many loops.- Closures capture surrounding variables and keep them alive.
- Closures stored for later must be marked
@escaping.
Exercise
You have the list of train fares [1250, 640, 2890, 455, 1999]. Using closures, print: the fares sorted from highest to lowest, the fares under ₹1,000 with 5 percent added (as whole rupees), and the total of all fares. Then write a function makeSeatAllocator(coach:) that returns a closure giving "S1-1", "S1-2" and so on, and print three seats.
Show solution
let fares = [1250, 640, 2890, 455, 1999]
print(fares.sorted(by: >))
print(fares.filter { $0 < 1000 }.map { $0 * 105 / 100 })
print(fares.reduce(0, +))
func makeSeatAllocator(coach: String) -> () -> String {
var seat = 0
return {
seat += 1
return "\(coach)-\(seat)"
}
}
let nextSeat = makeSeatAllocator(coach: "S1")
print(nextSeat())
print(nextSeat())
print(nextSeat())[2890, 1999, 1250, 640, 455] [672, 477] 7234 S1-1 S1-2 S1-3
640 * 105 / 100 is 672 and 455 * 105 / 100 is 477 in integer maths. The allocator closure captures seat and increases it on every call.
Quick Quiz
1. What does $0 mean inside a closure?
$0, $1 and so on are shorthand names for the closure's arguments.
2. What does ["1", "x", "3"].compactMap { Int($0) } return?
compactMap drops the nil results and unwraps the rest.
3. When must a closure parameter be marked @escaping?
Non-escaping closures must be used before the function returns.
4. What is a trailing closure?
Trailing closure syntax moves the last closure argument outside the brackets for readability.
Learn Swift with a mentor
Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
