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.

  • A closure can take input values and return an output value.
  • A closure can be anonymous, which means it does not need a function name.
  • A closure can capture values from the scope where it was created.
  • Many Swift standard library methods, such as sorted, map, and filter, rely on closures.
  • Closures are closely related to functions. In Swift, functions are actually a special kind of closure with a name.

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:

  • You want to customize how a function behaves.
  • You need to run code later, such as after a task finishes.
  • You are transforming or filtering collections.
  • You want small pieces of logic close to where they are used.

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

The general closure expression syntax in Swift looks like this:

Swift
{ (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:

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

let result = add(4, 6)
print(result)
Output✓ Tested · Swift 6.4
10

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:

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

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

print(doubled)
Output✓ Tested · Swift 6.4
10

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:

Swift
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.

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

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

print(highestFirst)
Output✓ Tested · Swift 6.4
[100, 92, 88, 75]

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.

Swift
let prices = [10, 25, 40]

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

print(formattedPrices)
Output✓ Tested · Swift 6.4
["$10", "$25", "$40"]

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.

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

let counterA = makeCounter()
print(counterA())
print(counterA())
print(counterA())
Output✓ Tested · Swift 6.4
1
2
3

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.

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

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

print(alphabetical)
Output✓ Tested · Swift 6.4
["Alex", "Morgan", "Taylor"]

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.

Swift
var savedAction: (() -> Void)?

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

storeAction {
    print("Action ran later")
}

savedAction?()
Output✓ Tested · Swift 6.4
Action ran later

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

  • Sorting product lists, dates, or scores with custom comparison rules.
  • Transforming API data into display-friendly strings with map.
  • Filtering arrays for search results with filter.
  • Passing completion handlers to code that performs work and responds later.
  • Building reusable utility functions that accept custom behavior from the caller.
  • Creating small stateful utilities, such as counters or accumulators, through captured values.

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.

Swift
let multiply = { (a: Int, b: Int) -> Int
    a * b
}
Compiler output✓ Tested · Swift 6.4
error: expected 'in' after the closure signature

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

Swift
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.

Swift
var completionHandler: (() -> Void)?

func saveHandler(handler: () -> Void) {
    completionHandler = handler
}
Compiler output✓ Tested · Swift 6.4
error: assigning non-escaping parameter 'handler' to an '@escaping' closure

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

Swift
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.

Swift
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.

Swift
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.

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

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

Swift
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:

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

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

Swift
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.

Swift
// 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.

Swift
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

  • Closures can make code harder to read if nested too deeply or shortened too aggressively.
  • Escaping closures require extra care because they may run later, after surrounding values have changed.
  • Stored closures in classes can create retain cycles when they strongly capture self.
  • Value capture can surprise beginners. A closure may keep using a captured variable even after the original scope has ended.
  • Type inference usually helps, but sometimes Swift reports errors such as Unable to infer complex closure return type or Contextual type for closure argument list expects 1 argument when the closure shape does not match the expected function type.
  • Non-escaping closures are safer and more efficient in some cases, but they cannot be stored for later use.

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.

Swift
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)
Output✓ Tested · Swift 6.4
["ALEX", "SAM", "TAYLOR"]

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

  • A closure is a self-contained block of code that can be passed around like a value.
  • Closures can take parameters, return values, and capture surrounding variables.
  • Functions are a named form of closure in Swift.
  • Trailing closure syntax improves readability when a closure is the last argument.
  • Use @escaping when a closure is stored or executed after the function returns.
  • Use capture lists such as [weak self] to avoid retain cycles in class-based code.
  • Short closure syntax is convenient, but readability should come first.

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.

  • Call the function with the value 3.
  • Use a closure that adds 2 to the input.
  • Print the result.

Expected output: 7

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

Show solution
Swift
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)
Output✓ Tested · Swift 6.4
7

Check Your Understanding

Pick an answer for each question, then check your score.

1. What is a closure in Swift?
Show answer

Answer: A self-contained block of code you can pass around like a value. Closures can take parameters, return values, and capture variables around them. Functions are named closures.

2. When must a closure parameter be marked @escaping?
Show answer

Answer: When it's stored or called after the function returns. Non-escaping closures must finish before the function returns. Storing one for later needs @escaping.

3. What is trailing closure syntax, as in numbers.map { $0 * 2 }?
Show answer

Answer: Writing the last closure argument after the call's parentheses. When a closure is the final argument, you can move it outside the parentheses, which reads more naturally.

4. Why do people write [weak self] in a closure?
Show answer

Answer: To avoid a retain cycle between an object and a closure it stores. A strong capture of self inside a closure the object keeps can stop both from ever being freed.

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.