Greetings, traveler!
We use iterators every day without thinking about them. Every for-in loop, every call to map or filter relies on the same mechanism. Swift hides most of the plumbing behind Sequence and IteratorProtocol, so the pattern feels almost invisible.
Still, it is worth understanding what actually happens under the hood. Once you see it clearly, custom traversal logic stops looking mysterious.
What the iterator pattern solves
The iterator pattern separates two concerns:
- how a collection stores its elements
- how those elements are accessed one by one
The client code should not care whether data lives in an array, a tree, a file stream, or a network buffer. It only needs a way to request the “next” element until there are no more.
In classic GoF terms:
- Aggregate → the container
- Iterator → the object that moves through elements
- Client → the code that consumes elements
In Swift:
Sequenceplays the role of AggregateIteratorProtocolrepresents Iteratorfor-inacts as the Client
Once you see that mapping, the pattern stops feeling abstract.
How Swift’s for-in loop actually works
A for-in loop:
for element in collection {
print(element)
}is roughly equivalent to:
var iterator = collection.makeIterator()
while let element = iterator.next() {
print(element)
}That is the whole story. The loop repeatedly calls next() until it returns nil.
So if you want custom iteration behavior, you control makeIterator() and next().
Implementing a custom iterator
Let’s build something slightly more interesting than a thin wrapper around an array. Imagine we have audio cassettes, but we only want to iterate over those released in the 90s.
First, define the model:
struct AudioCassette {
let title: String
let year: Int
}Now create a container:
struct AudioCassettes {
private let storage: [AudioCassette]
init(_ storage: [AudioCassette]) {
self.storage = storage
}
}Next, define the iterator:
struct NinetiesIterator: IteratorProtocol {
private let storage: [AudioCassette]
private var currentIndex: Int = 0
init(storage: [AudioCassette]) {
self.storage = storage
}
mutating func next() -> AudioCassette? {
while currentIndex < storage.count {
defer { currentIndex += 1 }
let cassette = storage[currentIndex]
if cassette.year >= 1990 && cassette.year < 2000 {
return cassette
}
}
return nil
}
}A few important details:
- The iterator is a
struct. next()is markedmutatingbecause it changes internal state.- The iterator decides which elements are visible.
Now connect the container to the iterator:
extension AudioCassettes: Sequence {
func makeIterator() -> NinetiesIterator {
NinetiesIterator(storage: storage)
}
}And use it:
let collection = AudioCassettes([
.init(title: "80s best disco music", year: 1985),
.init(title: "90s best electronic music", year: 1994),
.init(title: "90s rock hits", year: 1998)
])
for cassette in collection {
print(cassette.title)
}Only the 90s albums are printed. The client code never sees the filtering logic.
Why the iterator is a struct
It is tempting to ask: why not make it a class?
In Swift, iterators are usually value types. Each for-in loop receives its own copy of the iterator. That means independent state and predictable behavior.
Because next() moves the cursor forward, it must be mutating. That single keyword makes it clear that the iterator carries state internally.
This design aligns well with Swift’s value semantics. Iteration becomes explicit and controlled.
When you actually need a custom iterator
If you already have an array, you rarely need to write your own iterator. Standard library tools are powerful enough for most tasks.
Custom iterators become useful when:
- elements are generated lazily
- data is expensive to compute
- traversal order is not trivial
- you want to expose only a filtered or transformed view
A common example is reading a file line by line without loading everything into memory. Another is traversing a tree in depth-first or breadth-first order.
In those cases, the iterator controls the navigation strategy.
Iteration strategy as a design choice
One subtle benefit of this pattern is flexibility. The container decides which iterator to provide.
You could expose multiple traversal strategies:
extension AudioCassettes {
func ninetiesOnly() -> some Sequence {
storage.filter { $0.year >= 1990 && $0.year < 2000 }
}
}Or you could define separate iterators for different rules.
The key idea is that traversal logic does not leak into client code. The collection remains in control.
Conclusion
The iterator pattern in Swift looks simple because the language already did most of the heavy lifting. Still, understanding how Sequence and IteratorProtocol cooperate gives you more control over data access.
Behind every for-in loop there is a small state machine advancing one element at a time. Once you see that, custom traversal logic becomes a deliberate design decision rather than a workaround.
And sometimes that small layer of control is exactly what you need.
Well, that’s all for now. I hope this was clear and at least somewhat interesting for you. In the meantime, let’s move on to the next article, which will discuss the Mediator Design Pattern.
Check out other posts in the Design Patterns series:
- Visitor Design Pattern in Swift
- Template Method Design Pattern in Swift
- Strategy Design Pattern in Swift
- State Design Pattern in Swift
- Observer Design Pattern in Swift
- Memento Design Pattern in Swift
- Mediator Design Pattern in Swift
- Command Design Pattern in Swift
- Chain of Responsibility Design Pattern in Swift
- Proxy Design Pattern in Swift
- FlyWeight Design Pattern in Swift
- Facade Design Pattern in Swift
- Decorator Design Pattern in Swift
- Composite Design Pattern in Swift
- Bridge Design Pattern in Swift
- Adapter Design Pattern in Swift
- Singleton Design Pattern in Swift
- Prototype Design Pattern in Swift
- Builder Design Pattern in Swift
- Abstract Factory Design Pattern in Swift
- Factory Method Design Pattern in Swift
- Design Patterns: Basics
