EthicsComputerCourses
Join training +91 90532 10052

Closures

Swift · Lesson 11 of 30 Beginner 6 min read Updated

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.

Swift
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)
Output
[91, 88, 67, 45]
true true true

All four lines do the same thing:

  1. Full form: parameter names, types and return type are written out.
  2. Inferred types: sorted(by:) expects (Int, Int) -> Bool, so the types and return can go.
  3. Shorthand names: $0 and $1 are the first and second arguments. When the closure is the last argument, it can move outside the brackets as a trailing closure.
  4. 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.

Swift
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")
Output
[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.

Swift
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")
}
Output
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.

Swift
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())
Output
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.

Swift
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()
}
Output
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 $0 in long closures. Name the parameters when the body is more than one simple line.
  • Using map when you need compactMap. map { Int($0) } gives [Int?]; compactMap gives [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, .sorted and .map on 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/$1 and 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:) and first(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
Swift
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())
Output
[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?

2. What does ["1", "x", "3"].compactMap { Int($0) } return?

3. When must a closure parameter be marked @escaping?

4. What is a trailing closure?

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