Swift Closures Explained: Syntax, Capture Values, and Uses

Swift closures are one of the most important language features to understand because they are used everywhere: sorting arrays, handling completion callbacks, transforming collections, and passing behavior into functions. In this article, you will learn what closures are, how Swift closure syntax works, how value capture behaves, and how to avoid the mistakes that beginners commonly make.

Quick answer: A closure in Swift is a self-contained block of code that you can store in a variable, pass to a function, and run later. You can think of it as a function value, and unlike a named function, a closure can capture values from the surrounding scope.

Difficulty: Intermediate

Helpful to know first: basic Swift syntax, how functions are declared and called, and common types like String, Int, and arrays.

1. What Is a Closure?

A closure is a reusable block of code that can be treated like a value. You can assign it to a variable, pass it as an argument, return it from another function, and execute it when needed.

That last point is important for beginners: closures and functions are not unrelated concepts. A named function can often be used anywhere Swift expects a closure with the same parameter and return types.

2. Why Closures Matters

Closures matter because they let you pass behavior, not just data. Instead of writing a function that always performs one hard-coded action, you can write a function that accepts a closure and lets the caller decide what should happen.

That makes code more flexible and more reusable. For example, rather than writing separate functions to sort numbers ascending and descending, you can write one sorting function and pass in the comparison logic as a closure.

Closures are especially useful when:

Closures are less appropriate when the logic is large, reused across many places, or deserves a descriptive name. In those cases, a named function is often clearer.

3. Basic Syntax or Core Idea

The general closure expression syntax in Swift looks like this:

{ (parameters) -> ReturnType in
    // code to run
}

The parameter list and return type come before the in keyword. The body of the closure comes after in.

A simple closure stored in a variable

Here is a basic closure that takes two integers and returns their sum:

let add = { (a: Int, b: Int) -> Int in
    return a + b
}

let result = add(4, 6)
print(result)

This closure is assigned to add. Because closures are values, you can call the variable like a function. The output is 10.

A function that accepts a closure

Closures become more useful when you pass them into functions:

func performOperation(
    on x: Int,
    using operation: (Int) -> Int
) -> Int {
    return operation(x)
}

let doubled = performOperation(on: 5) { number in
    number * 2
}

print(doubled)

The parameter named operation expects a closure that takes one Int and returns an Int. The trailing closure syntax makes the function call easier to read.

Closures vs functions

A named function can often replace a closure:

func double(number: Int) -> Int {
    number * 2
}

let value = performOperation(on: 5, using: double)

Use closures when the logic is short and local. Use a named function when the behavior needs a meaningful name or will be reused.

4. Step-by-Step Examples

Example 1: Sorting an array

One of the most common closure examples is sorting. The closure decides whether one element should come before another.

let scores = [88, 92, 75, 100]

let highestFirst = scores.sorted { first, second in
    first > second
}

print(highestFirst)

The closure receives two elements and returns true when the first should appear before the second. The result is a descending array.

Example 2: Transforming values with map

The map method uses a closure to transform each element into a new value.

let prices = [10, 25, 40]

let formattedPrices = prices.map { price in
    "$\(price)"
}

print(formattedPrices)

Each integer becomes a string. The closure defines the transformation for one item, and map applies it to the whole array.

Example 3: Capturing values from the surrounding scope

Closures can remember values from the place where they were created. This is called capturing values.

func makeCounter() -> () -> Int {
    var count = 0
    
    let counter = {
        count += 1
        return count
    }
    
    return counter
}

let counterA = makeCounter()
print(counterA())
print(counterA())
print(counterA())

Even after makeCounter finishes, the closure still remembers count. This is one of the most powerful differences between closures and simple code blocks.

Example 4: Using shorthand argument names

Swift lets you shorten closure syntax when types are already known from context.

let names = ["Taylor", "Alex", "Morgan"]

let alphabetical = names.sorted { $0 < $1 }

print(alphabetical)

Here, $0 means the first argument and $1 means the second. This style is compact, but it should be used only when the meaning stays obvious.

Example 5: Escaping closures for later execution

A closure is escaping when the function stores it or uses it after the function returns.

var savedAction: (() -> Void)?

func storeAction(action: @escaping () -> Void) {
    savedAction = action
}

storeAction {
    print("Action ran later")
}

savedAction?()

The @escaping attribute is required because the closure outlives the function call. This is common in completion handlers and asynchronous code.

5. Practical Use Cases

6. Common Mistakes

Mistake 1: Forgetting the in keyword in a full closure expression

When you write an explicit parameter list in a closure, Swift expects the in keyword before the body.

Problem: This closure declares parameters and a return type, but it does not separate the signature from the body with in, so Swift cannot parse it correctly.

let multiply = { (a: Int, b: Int) -> Int
    a * b
}

Fix: Add in between the signature and the closure body.

let multiply = { (a: Int, b: Int) -> Int in
    a * b
}

The corrected version works because Swift now knows where the closure signature ends and the executable body begins.

Mistake 2: Using an escaping closure without @escaping

If a function stores a closure for later use, the closure escapes the function body and must be marked appropriately.

Problem: This code assigns the closure to external storage, so Swift reports an error like Escaping closure captures non-escaping parameter unless the parameter is marked @escaping.

var completionHandler: (() -> Void)?

func saveHandler(handler: () -> Void) {
    completionHandler = handler
}

Fix: Mark the closure parameter with @escaping when it will be stored or executed after the function returns.

var completionHandler: (() -> Void)?

func saveHandler(handler: @escaping () -> Void) {
    completionHandler = handler
}

The corrected version works because Swift now knows the closure may live beyond the function call.

Mistake 3: Capturing self strongly in a stored closure

Closures capture referenced values by default. In class-based code, this can create a retain cycle if an object stores a closure that also strongly captures that same object.

Problem: The instance keeps the closure alive, and the closure keeps the instance alive. That prevents deallocation and can cause a memory leak.

class Greeter {
    var name: String
    var sayHello: (() -> Void)?
    
    init(name: String) {
        self.name = name
        self.sayHello = {
            print("Hello, \(self.name)")
        }
    }
}

Fix: Use a capture list such as [weak self] when a stored closure should not keep the object alive.

class Greeter {
    var name: String
    var sayHello: (() -> Void)?
    
    init(name: String) {
        self.name = name
        self.sayHello = { [weak self] in
            guard let self = self else { return }
            print("Hello, \(self.name)")
        }
    }
}

The corrected version works because the closure no longer holds a strong reference that would keep the instance in memory.

Mistake 4: Overusing shorthand arguments until the closure becomes unclear

Shorthand names like $0 and $1 are useful, but they can hurt readability when the logic is more than very simple.

Problem: This closure is compact but hard to understand at a glance, especially for beginners or future maintainers.

let result = ["bob", "alice", "sam"].sorted {
    $0.lowercased() < $1.lowercased()
}

Fix: Use explicit parameter names when they make the logic easier to read.

let result = ["bob", "alice", "sam"].sorted { leftName, rightName in
    leftName.lowercased() < rightName.lowercased()
}

The corrected version works because the closure still does the same job while making the comparison easier to understand.

7. Best Practices

Practice 1: Prefer the simplest closure syntax that stays readable

Swift supports several levels of shorthand. Use only as much shortening as the reader can still understand easily.

A less readable style can be too compact:

let sortedNames = names.sorted { $0 < $1 }

A clearer version may be better when the code is teaching-oriented or slightly more complex:

let sortedNames = names.sorted { firstName, secondName in
    firstName < secondName
}

This matters because code is read far more often than it is written.

Practice 2: Use trailing closure syntax when it improves the call site

When a function’s last argument is a closure, trailing closure syntax usually makes the code more natural.

// Less preferred when the closure is the final argument
let values = [1, 2, 3].map({ number in
    number * 2
})

// Preferred
let doubledValues = [1, 2, 3].map { number in
    number * 2
}

This style is common across Swift codebases, especially with collection methods and completion handlers.

Practice 3: Think carefully about capture semantics

Closures capture values automatically, which is powerful but easy to misuse. Be intentional about whether the closure should keep something alive.

class Logger {
    var message = "Ready"
    
    func makePrinter() -> () -> Void {
        return { [weak self] in
            print(self?.message ?? "No message")
        }
    }
}

This is especially important with classes and stored closures, where strong reference cycles are possible.

8. Limitations and Edge Cases

If a closure seems confusing, try adding explicit parameter types and a return type first. Once the code is working, you can simplify it carefully.

9. Practical Mini Project

This mini project builds a simple list processor. It uses closures to filter, transform, and sort strings, which mirrors common real-world Swift code.

func processNames(
    _ names: [String],
    filter: (String) -> Bool,
    transform: (String) -> String,
    sortBy: (String, String) -> Bool
) -> [String] {
    let filtered = names.filter(filter)
    let transformed = filtered.map(transform)
    return transformed.sorted(by: sortBy)
}

let names = ["Taylor", "alex", "Sam", "jo"]

let result = processNames(
    names,
    filter: { name in
        name.count >= 3
    },
    transform: { name in
        name.uppercased()
    },
    sortBy: { left, right in
        left < right
    }
)

print(result)

This example shows why closures are so useful: the function itself stays generic, while the caller defines the behavior. You can change filtering, formatting, or sorting without rewriting the function.

10. Key Points

11. Practice Exercise

Create a function named applyTwice that accepts an Int and a closure of type (Int) -> Int. The function should apply the closure to the number two times and return the final result.

Expected output: 7

Hint: Run the closure once, store the intermediate result, then run it again on that result.

func applyTwice(to value: Int, using operation: (Int) -> Int) -> Int {
    let firstResult = operation(value)
    let secondResult = operation(firstResult)
    return secondResult
}

let result = applyTwice(to: 3) { number in
    number + 2
}

print(result)

12. Final Summary

Swift closures let you write behavior as a value. That means you can store code, pass it into functions, return it from functions, and execute it later. Once you understand closure syntax, trailing closures, shorthand arguments, and value capture, a huge amount of Swift code becomes much easier to read.

The most important ideas to keep in mind are that closures can capture surrounding values, escaping closures live beyond the function call, and stored closures in classes may need capture lists like [weak self]. If you want to get more comfortable with closures, a strong next step is to practice them with collection methods such as map, filter, reduce, and sorted.