Closures & Collection Iteration: map, filter, reduce

SwiftChapter 10 of the Ultimate Swift Series32 min readApril 11, 2026Intermediate

In This Article

  1. What Is a Closure?
  2. Closure Syntax and Shorthand
  3. Trailing Closure Syntax
  4. Capturing Values From Surrounding Scope
  5. Custom Sorting With Closures
  6. forEach: Looping Functionally
  7. filter: Keeping What Matters
  8. map: Transforming Every Element
  9. compactMap and flatMap
  10. reduce: Collapsing to a Single Value
  11. Chaining Operations
  12. Lazy Collections
  13. Exercises
  14. Key Points

In the previous chapter, you used for-in loops to iterate over arrays and dictionaries. Closures unlock a more powerful, expressive way to work with collections — transforming, filtering, and reducing data in just a few lines. This chapter introduces closures and the functional operations built on top of them.

What Is a Closure?

A closure is a function without a name. You can assign it to a variable, pass it to a function, and return it from a function — just like any other value. Closures are called "closures" because they can close over (capture) variables and constants from the surrounding scope.

// A regular function func multiply(_ a: Int, _ b: Int) -> Int { a * b } // The same logic as a closure let multiplyClosure = { (a: Int, b: Int) -> Int in a * b } let result = multiplyClosure(4, 2) // 8

The closure looks like a function body wrapped in braces. The keyword in separates the parameter list and return type from the body. The type of multiplyClosure is (Int, Int) -> Int — the same type as the function version.

Closure Syntax and Shorthand

Swift provides progressively shorter ways to write closures. Starting from the full form:

// Full form let multiply = { (a: Int, b: Int) -> Int in return a * b } // Omit return (single expression) let multiply = { (a: Int, b: Int) -> Int in a * b } // Omit types (when Swift can infer them) let multiply: (Int, Int) -> Int = { (a, b) in a * b } // Omit parameter names (use $0, $1, ...) let multiply: (Int, Int) -> Int = { $0 * $1 }

The $0, $1 shorthand refers to the first, second (etc.) parameters by position. Use this only for short closures where the meaning is obvious.

Trailing Closure Syntax

When a closure is the last parameter of a function call, you can pull it out of the parentheses. This is called trailing closure syntax and is extremely common in Swift:

func operateOnNumbers(_ a: Int, _ b: Int, operation: (Int, Int) -> Int) -> Int { operation(a, b) } // Without trailing closure operateOnNumbers(4, 2, operation: { $0 + $1 }) // With trailing closure — cleaner operateOnNumbers(4, 2) { $0 + $1 }

You'll see trailing closure syntax everywhere in Swift — sorting, filtering, animations, SwiftUI views. It's one of the features that makes Swift code read so naturally.

Capturing Values From Surrounding Scope

The defining feature of closures: they can access and modify variables from their enclosing scope.

var counter = 0 let increment = { counter += 1 } increment() // counter = 1 increment() // counter = 2 increment() // counter = 3

The closure captures counter — it holds a reference to the same variable, not a copy. Changes inside the closure are visible outside, and vice versa.

This enables powerful patterns like closure factories:

func makeCounter() -> () -> Int { var count = 0 return { count += 1 return count } } let counter1 = makeCounter() let counter2 = makeCounter() counter1() // 1 counter1() // 2 counter2() // 1 — independent counter

Each call to makeCounter() creates a new, independent count variable. Each returned closure captures its own copy.

Custom Sorting With Closures

let names = ["ZZZZZZ", "BB", "A", "CCCC", "EEEEE"] // Default: alphabetical names.sorted() // ["A", "BB", "CCCC", "EEEEE", "ZZZZZZ"] // Custom: sort by string length (longest first) names.sorted { $0.count > $1.count } // ["ZZZZZZ", "EEEEE", "CCCC", "BB", "A"]

The closure tells sorted how to compare two elements. Return true if the first should come before the second.

forEach: Looping Functionally

let values = [1, 2, 3, 4, 5] values.forEach { print("\($0) squared = \($0 * $0)") } // 1 squared = 1 // 2 squared = 4 // ...

forEach is an alternative to for-in loops. Use whichever reads better — for-in is more common for complex logic, forEach for simple one-liners.

filter: Keeping What Matters

filter returns a new array containing only elements that pass a test:

let prices = [1.5, 10.0, 4.99, 2.30, 8.19] let expensive = prices.filter { $0 > 5 } // [10.0, 8.19] // Find first match let firstExpensive = prices.first { $0 > 5 } // Optional(10.0)

The closure takes each element and returns true to keep it or false to exclude it. The original array is never modified.

map: Transforming Every Element

map applies a transformation to every element and returns a new array of results:

let prices = [1.5, 10.0, 4.99, 2.30, 8.19] // 10% discount on every item let salePrice = prices.map { $0 * 0.9 } // [1.35, 9.0, 4.491, 2.07, 7.371] // Transform to a different type let labels = prices.map { "$\($0)" } // ["$1.5", "$10.0", "$4.99", "$2.3", "$8.19"]

map preserves the count and order. Each element is transformed independently.

compactMap and flatMap

compactMap: map + remove nils

let input = ["0", "11", "haha", "42"] let parsed = input.compactMap { Int($0) } // [0, 11, 42] — "haha" couldn't convert, so it's dropped

compactMap works like map but automatically filters out nil results. Perfect for parsing or converting data where some values might be invalid.

flatMap: flatten nested collections

let nested = [[1, 2], [3, 4], [5]] let flat = nested.flatMap { $0 } // [1, 2, 3, 4, 5]

flatMap takes a closure that returns a collection, then concatenates all the results into a single flat array.

reduce: Collapsing to a Single Value

reduce combines all elements into a single value using an accumulator:

let prices = [1.5, 10.0, 4.99, 2.30, 8.19] // Sum all prices let total = prices.reduce(0) { $0 + $1 } // 26.98 // Even shorter — + is just a function let total2 = prices.reduce(0, +) // 26.98

The first argument (0) is the initial value. The closure receives the running total ($0) and the current element ($1), returning the new running total.

reduce works on dictionaries too:

let stock = ["Apples": 5, "Bananas": 12, "Oranges": 8] let totalItems = stock.reduce(0) { $0 + $1.value } // 25

Chaining Operations

The real power emerges when you chain these operations together:

let scores = [85, 42, 91, 67, 73, 95, 38] // Get passing scores (>= 60), add 5 bonus points, then sum let adjustedTotal = scores .filter { $0 >= 60 } // [85, 91, 67, 73, 95] .map { $0 + 5 } // [90, 96, 72, 78, 100] .reduce(0, +) // 436

Three operations, three lines, no loops, no temporary variables. Each operation returns a new collection that feeds into the next.

Lazy Collections

What if your collection is huge (or infinite)? Adding .lazy tells Swift to evaluate elements on demand instead of all at once:

func isPrime(_ n: Int) -> Bool { if n < 2 { return false } for i in 2...Int(Double(n).squareRoot()) { if n % i == 0 { return false } } return true } // Get the first 10 prime numbers — lazily evaluated let primes = (1...).lazy .filter { isPrime($0) } .prefix(10) primes.forEach { print($0) } // 2, 3, 5, 7, 11, 13, 17, 19, 23, 29

Without .lazy, Swift would try to filter an infinite range — which would never finish. With .lazy, it only evaluates elements as needed, stopping after finding 10 primes.

When to use lazy

Use .lazy when (1) the collection is very large, (2) computation per element is expensive, or (3) you only need a subset of results. For small collections, regular (eager) evaluation is fine and simpler to debug.

Exercises

Try These in Your Playground

  1. Create an array of names. Use reduce to concatenate them into a single string separated by commas.
  2. Using the same array, chain filter (names longer than 4 characters) and reduce (concatenate) together.
  3. Create a dictionary of names to ages. Use filter to get only people under 18, then use map to extract just the names into an array.
  4. Write a function repeatTask(times: Int, task: () -> Void) that runs a closure a given number of times. Use it to print a message 5 times.
  5. Use compactMap to convert ["1", "abc", "3", "def", "5"] into an [Int].
  6. Challenge: Use reduce to find the sum of the first 10 Fibonacci numbers without using a loop.

Key Points

What You Learned

This completes Section II of the Swift fundamentals. In the next chapter, we begin Section III: Building Your Own Types, starting with Structs — Swift's primary tool for modeling data.

Watch the video lessons

Our Swift Fundamentals course covers closures, functional patterns, and real-world collection operations in 96 video lessons.

Watch Swift Videos