Closures are self-contained pieces of code that you can store and pass around your codebase. Think of them as unnamed functions that can capture and store references to any constants and variables from the context in which they’re defined. If you’ve used functions in Swift, then you’ll soon grasp closures as they are actually a special case of closures.

Closures are used very often in iOS development. You’ll see them in: completion handlers, animations, collection methods like map and filter, and SwiftUI view builders. Let’s look at them in more detail.

What are Closures?

A closure is a block of code that can be stored in a variable, passed as a parameter, or called later.

Here’s the basic syntax:

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

Simple Example:

let greetingClosure = { (name: String) -> String in
    return "Hello, \(name)!"
}

let message = greetingClosure("Swift")
print(message) // "Hello, Swift!"

In the above example, calling the greetingClosure looks like calling a function and passing a String as a parameter.


Closure Syntax Simplification

Swift provides several ways to simplify closure syntax, making them more concise and readable.

1. Type Inference

When the compiler can infer the types, you can omit them:

let numbers = [1, 2, 3, 4, 5]

// Explicit types
let doubled = numbers.map({ (number: Int) -> Int in
    return number * 2
})

// Type inference
let doubled = numbers.map({ number in
    return number * 2
})

2. Implicit Return

For single-expression closures, you can omit the return keyword:

let doubled = numbers.map({ number in number * 2 })

3. Shorthand Argument Names

Swift provides automatic names for closure parameter if don’t specify their names. The notation to access parameters is $0, $1, $2, etc. Where $0 is the first parameter, $1 is the second parameter and so on.

let doubled = numbers.map({ $0 * 2 })

4. Trailing Closure Syntax

When a closure is the last parameter of a function, you can write it outside the parentheses:

// Traditional syntax
let doubled = numbers.map({ $0 * 2 })

// Trailing closure syntax
let doubled = numbers.map { $0 * 2 }

Common Use Cases

Completion Handlers

Closures are perfect for handling asynchronous operations because we don’t know when the operation will finish. So if we pass a closure, the asynchronous operation and call our closure when it is finished.

In the below example, we try to fetch Data from a url. When the operation finishes it calls the completion closure passing in a Result.

func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
        } else if let data = data {
            completion(.success(data))
        }
    }.resume()
}

// Usage
fetchData { result in
    switch result {
    case .success(let data):
        print("Received data: \(data)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

Collection Methods

Closures make working with collections incredibly powerful. Here’s how you can transform, filter, and aggregate data with just a few lines of code:

let scores = [85, 92, 78, 96, 88]

// Filter passing scores
let passingScores = scores.filter { $0 >= 80 }

// Transform to letter grades
let letterGrades = scores.map { score in
    switch score {
    case 90...100: return "A"
    case 80...89: return "B"
    case 70...79: return "C"
    default: return "F"
    }
}

// Find the sum
let totalScore = scores.reduce(0) { $0 + $1 }

In this example, filter keeps only scores above 80, map transforms each numeric score into a letter grade, and reduce combines all scores into a single total. Notice how each method takes a closure that defines the specific operation to perform on each element.

UI Animations

Closures are essential for UIKit animations. They define what should happen during the animation and what to do when it completes:

UIView.animate(withDuration: 0.3) {
    self.viewToAnimate.alpha = 0.0
    self.viewToAnimate.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
} completion: { _ in
    self.viewToAnimate.isHidden = true
}

The first closure defines the changes that should be animated in this case, fading out and scaling down a view. The completion closure runs after the animation finishes, here hiding the view entirely. This pattern is common throughout UIKit for handling the before and after states of animations.


Capturing Values

Closures can capture and store references to any constants and variables from their surrounding context. This is called capturing. It’s one of the most powerful features of closures. They remember the environment they were created in.

Example:

func makeIncrementer(incrementAmount: Int) -> () -> Int {
    var total = 0
    
    let incrementer = {
        total += incrementAmount
        return total
    }
    
    return incrementer
}

let incrementByTwo = makeIncrementer(incrementAmount: 2)
print(incrementByTwo()) // 2
print(incrementByTwo()) // 4
print(incrementByTwo()) // 6

Here’s what’s happening: the closure captures both total and incrementAmount from its surrounding context. Even after the makeIncrementer function returns, the closure still has access to these values. Each time we call incrementByTwo(), it modifies the captured total variable and returns the new value. This is why we see 2, then 4, then 6 the closure maintains its own copy of the captured variables.


Escaping vs Non-Escaping Closures

By default, closures passed as parameters are non-escaping, meaning they must be called before the function returns. For closures that need to be called after the function returns (like completion handlers), use @escaping.

// Non-escaping (default)
func processImmediately(with closure: () -> Void) {
    closure() // Must be called before function returns
}

// Escaping
func processLater(with closure: @escaping () -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        closure() // Called after function returns
    }
}

The key difference: processImmediately calls the closure right away, while processLater stores the closure and calls it later via DispatchQueue. Since the closure outlives the function call, it needs to be marked as @escaping. This is common with network requests, timers, and any asynchronous operations where you don’t know exactly when the work will complete.


Memory Management and Capture Lists

When closures capture reference types, they can create strong reference cycles that prevent objects from being deallocated. Use capture lists to avoid memory leaks:

class ViewController: UIViewController {
    func setupButton() {
        button.onTap = { [weak self] in
            self?.handleButtonTap()
        }
        
        // Or with unowned for when you're certain self won't be nil
        button.onTap = { [unowned self] in
            self.handleButtonTap()
        }
    }
}

In the first example, [weak self] creates a weak reference to the view controller. If the view controller gets deallocated, self becomes nil and we safely handle it with optional chaining (self?.). The second example uses [unowned self], which is appropriate when you’re certain that self will never be nil when the closure executes. However, if you’re wrong, your app will crash so use weak unless you’re absolutely sure.


Best Practices

1. Use Trailing Closure Syntax

When a closure is the last parameter, use trailing closure syntax for better readability:

// Preferred
numbers.filter { $0 > 10 }

// Less preferred
numbers.filter({ $0 > 10 })

The trailing closure syntax removes visual clutter and makes your code more readable, especially when chaining multiple operations. Most Swift developers expect this style, so it’s worth adopting consistently.

2. Keep Closures Short

If your closure becomes complex, consider extracting it into a separate function. This improves readability and makes your code easier to test:

// Complex closure harder to read
let processedData = data.map { item in
    let processed = performComplexOperation(on: item)
    let validated = validateResult(processed)
    return transformFinalResult(validated)
}

// Better extract to a function
let processedData = data.map(processItem)

func processItem(_ item: DataItem) -> ProcessedItem {
    let processed = performComplexOperation(on: item)
    let validated = validateResult(processed)
    return transformFinalResult(validated)
}

The extracted function approach has several benefits: it’s easier to unit test processItem in isolation, the logic is reusable elsewhere, and the map call clearly communicates its intent without getting bogged down in implementation details.

3. Be Mindful of Capture Lists

Always consider whether you need [weak self] or [unowned self] to avoid retain cycles. This is especially important in view controllers and any long-lived objects:

// Good prevents retain cycles
networkManager.fetchData { [weak self] result in
    self?.handleResult(result)
}

When in doubt, use [weak self]. It’s safer than [unowned self] and the optional chaining syntax makes it clear that self might be nil. This is particularly important for network requests, timers, and any closures that might outlive the object that created them.


Closures are fundamental to Swift programming and essential for iOS development. They enable powerful functional programming patterns, make asynchronous code more manageable, and provide elegant solutions for many common programming tasks. Master closures, and you’ll find your Swift code becomes more expressive and powerful.