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.