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