Skip to main content

Swift Closures

Introduction

Closures are self-contained blocks of functionality that can be passed around and used in your code. Think of them as mini-functions that don't need a name. If you're familiar with other programming languages, closures are similar to lambda functions in Python, blocks in Ruby, or anonymous functions in JavaScript.

In Swift, closures are powerful and elegant constructs that help you write cleaner, more concise code. They're particularly useful when working with Swift's standard library functions and iOS frameworks like UIKit, which often take closures as arguments for callbacks, completion handlers, and more.

Understanding Closures

What is a Closure?

A closure is a self-contained block of code that can be assigned to variables, passed as arguments to functions, or returned from functions. They "close over" the variables and constants from the surrounding context, capturing and storing references to them.

Closure Syntax

Here's the general syntax of a Swift closure:

swift
{ (parameters) -> returnType in
// Code to be executed
}

Let's break it down:

  • Enclosed in curly braces {}
  • Parameters listed inside parentheses ()
  • The -> arrow followed by the return type
  • The in keyword separating the definition from the body
  • The body contains the code to be executed

Basic Closure Examples

Example 1: A Simple Closure

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

// Using the closure
let greeting = greet("John")
print(greeting) // Output: Hello, John!

Example 2: Using a Closure as a Function Parameter

swift
func performOperation(on numbers: [Int], using operation: (Int) -> Int) -> [Int] {
var result = [Int]()

for number in numbers {
result.append(operation(number))
}

return result
}

// Using the function with a closure
let numbers = [1, 2, 3, 4, 5]
let squaredNumbers = performOperation(on: numbers) { (number) -> Int in
return number * number
}

print(squaredNumbers) // Output: [1, 4, 9, 16, 25]

Closure Syntax Optimization

Swift offers several ways to simplify closure syntax. Let's explore them with an example.

Starting Point: Verbose Syntax

swift
let numbers = [1, 2, 3, 4, 5]
let sortedNumbers = numbers.sorted(by: { (num1: Int, num2: Int) -> Bool in
return num1 > num2
})
print(sortedNumbers) // Output: [5, 4, 3, 2, 1]

Optimization 1: Inferring Type from Context

Since Swift can infer the parameter types and return type:

swift
let sortedNumbers = numbers.sorted(by: { (num1, num2) in
return num1 > num2
})

Optimization 2: Implicit Return for Single-Expression Closures

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

swift
let sortedNumbers = numbers.sorted(by: { (num1, num2) in num1 > num2 })

Optimization 3: Shorthand Argument Names

Swift provides shorthand argument names for closure parameters: $0, $1, $2, etc.:

swift
let sortedNumbers = numbers.sorted(by: { $0 > $1 })

Optimization 4: Operator Methods

When the closure is just performing a comparison with a standard operator:

swift
let sortedNumbers = numbers.sorted(by: >)

Optimization 5: Trailing Closure Syntax

When a closure is the last parameter of a function, you can use trailing closure syntax:

swift
// Instead of:
let sortedNumbers = numbers.sorted(by: { $0 > $1 })

// You can write:
let sortedNumbers = numbers.sorted { $0 > $1 }

Capturing Values in Closures

One of the most powerful features of closures is their ability to capture values from their surrounding context.

swift
func makeIncrementer(incrementAmount: Int) -> () -> Int {
var total = 0

let incrementer = { () -> Int in
total += incrementAmount
return total
}

return incrementer
}

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

In this example:

  1. makeIncrementer returns a closure that increments a counter
  2. The closure captures total and incrementAmount from its surrounding context
  3. Even after makeIncrementer finishes execution, the closure still has access to these values
  4. Each time we call incrementByTwo(), it increments and returns the captured total

Escaping and Non-Escaping Closures

Non-escaping Closures

By default, closures passed as function arguments are non-escaping, meaning they must be executed within the function's lifetime.

swift
func performOperation(with completion: () -> Void) {
// Some work...
completion()
// completion must be called before this function returns
}

Escaping Closures

When a closure needs to outlive the function it's passed to, you use @escaping:

swift
func fetchData(completion: @escaping (Data) -> Void) {
// Simulating async operation
DispatchQueue.global().async {
let data = Data() // placeholder
// This is called after fetchData has returned
completion(data)
}
}

// Usage
fetchData { data in
print("Received \(data.count) bytes")
}

Common use cases for escaping closures:

  • Asynchronous operations
  • Storage in properties
  • Callbacks that execute after a function returns

Autoclosures

Swift provides a special attribute @autoclosure that automatically creates a closure from an expression:

swift
func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String) {
if !condition() {
print(message())
}
}

// Usage - no closure syntax needed
assert(2 + 2 == 5, "Math is broken!")

This is handy for functions that take closures but you want the caller to pass simple expressions.

Practical Examples

Example 1: Filter, Map, and Reduce

swift
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// Filter: Keep only even numbers
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers) // Output: [2, 4, 6, 8, 10]

// Map: Double each number
let doubledNumbers = numbers.map { $0 * 2 }
print(doubledNumbers) // Output: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// Reduce: Sum all numbers
let sum = numbers.reduce(0) { $0 + $1 }
print(sum) // Output: 55

Example 2: Animation with Closures

Here's how closures are commonly used in iOS development for animations:

swift
// iOS UI animation example
UIView.animate(withDuration: 0.5, animations: {
someView.alpha = 0
}, completion: { finished in
if finished {
someView.removeFromSuperview()
}
})

// With trailing closure syntax
UIView.animate(withDuration: 0.5) {
someView.alpha = 0
} completion: { finished in
if finished {
someView.removeFromSuperview()
}
}

Example 3: Custom Sorting

swift
let students = [
("John", 85),
("Alice", 92),
("Bob", 78),
("Eve", 95)
]

// Sort by grades in descending order
let sortedByGrade = students.sorted { $0.1 > $1.1 }
print(sortedByGrade)
// Output: [("Eve", 95), ("Alice", 92), ("John", 85), ("Bob", 78)]

// Sort by name in alphabetical order
let sortedByName = students.sorted { $0.0 < $1.0 }
print(sortedByName)
// Output: [("Alice", 92), ("Bob", 78), ("Eve", 95), ("John", 85)]

Common Closure Patterns in Swift

Completion Handlers

Completion handlers let you execute code after a task completes:

swift
func processData(data: String, completion: (Bool, String) -> Void) {
// Process the data
let success = true
let result = "Processed: \(data)"

completion(success, result)
}

// Usage
processData(data: "raw data") { success, result in
if success {
print("Success: \(result)")
} else {
print("Failed to process data")
}
}

Lazy Properties with Closures

You can use closures to initialize complex properties only when they're needed:

swift
class DataManager {
lazy var expensiveResource: [String: Any] = {
// Complex initialization code
var result = [String: Any]()
// Populate the dictionary...
return result
}()
}

Summary

Closures are one of Swift's most powerful features, allowing you to write more concise, flexible, and expressive code. They're essential for iOS and macOS development, especially for handling asynchronous tasks, callbacks, and event-driven programming.

Key points to remember:

  • Closures are self-contained blocks of code that can be passed around
  • They can capture and store references to variables and constants from their surrounding context
  • Swift offers many syntax optimizations to make closures more readable
  • Closures can be non-escaping (default) or escaping (with @escaping)
  • Common use cases include completion handlers, callbacks, and higher-order functions

Exercises

  1. Write a closure that takes an integer and returns true if it's a prime number.
  2. Create a function that takes an array of integers and a closure, and returns a new array containing only the elements for which the closure returns true.
  3. Write a function that returns a closure which acts as a counter (similar to the makeIncrementer example).
  4. Use map, filter, and reduce together to transform an array of strings to an integer representing the count of strings that have more than 5 characters.

Additional Resources

Happy coding with Swift closures!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)