Software Design

Swift Iterator Pattern: How Sequence Works

When building apps with Swift, you write loops like for item in array dozens of times a day. Because they feel so natural, you may only start wondering how they work after a custom type fails to compile in a for-in loop: "What is actually happening inside this loop?"

4 min read
Cover image for Swift Iterator Pattern: How Sequence Works

When building apps with Swift, you write loops like for item in array dozens of times a day. Because they feel so natural, you may only start wondering how they work after a custom type fails to compile in a for-in loop: “What is actually happening inside this loop?”

To start with the conclusion, Swift’s for-in loop is the result of two protocols working together: Sequence and IteratorProtocol. Sequence declares, “I can be iterated,” while IteratorProtocol actually produces values by saying, “I’ll give you the next one.”

This article explains how the two protocols divide responsibilities, how for-in is lowered internally, and how to build a custom sequence yourself. If the iterator pattern has ever felt difficult, read on.

Sequence and IteratorProtocol: What’s the difference?

Let’s start with the most confusing point. Both involve iteration, but their roles are clearly different.

IteratorProtocol has exactly one requirement: mutating func next() -> Element?. Each call returns one next element, and returns nil when there is nothing left.

In other words, an iterator is a cursor that carries state. It remembers how far it has read and advances one position whenever next() is called.

Sequence operates at a higher level. Its role is to create and provide an iterator through the makeIterator() method. It effectively says, “Use this iterator if you want to iterate over me.”

In summary:

  • IteratorProtocol: A cursor that retrieves one value at a time. next() is the key.
  • Sequence: A factory that creates the cursor. makeIterator() is the key.

Arrays, dictionaries, Sets, strings, ranges, and every other iterable type in the Swift standard library conform to Sequence.


How is for-in lowered internally?

This is the core of the article. The for-in syntax we use without thinking is actually syntactic sugar.

The code below is the loop we normally write.

for number in [1, 2, 3] {
    print(number)
}

The compiler roughly transforms it into the form below. It creates an iterator with makeIterator() and runs a while loop until next() returns nil.

var iterator = [1, 2, 3].makeIterator()
while let number = iterator.next() {
    print(number)  // 1, 2, 3  order output
}

for-in is not magic. It is ultimately just a combination of makeIterator() and next().

So these two calls were all it took
So these two calls were all it took

Once you understand this structure, it becomes clear why a type that only conforms to Sequence can be used directly in for-in. for-in only needs makeIterator().


How do you create a custom sequence?

Building one yourself makes the idea click. Let’s use a sequence that generates Fibonacci numbers indefinitely as an example.

The simplest approach is for one small type to conform to both Sequence and IteratorProtocol. This lets makeIterator() use its default implementation, which returns a copy of itself.

struct Fibonacci: Sequence, IteratorProtocol {
    var current = 0
    var nextValue = 1
    mutating func next() -> Int? {
        defer { (current, nextValue) = (nextValue, current + nextValue) }
        return current
    }
}

next() is straightforward. Just before returning the current value, it uses defer to calculate the next state in advance.

This type can now be used directly in for-in. Since the sequence is infinite, you need to limit the iteration count yourself.

for n in Fibonacci().prefix(8) {
    print(n)  // 0 1 1 2 3 5 8 13
}
I wrote a Fibonacci type and used it in for-in
I wrote a Fibonacci type and used it in for-in

It is also important that you get methods such as prefix, map, and filter for free. As soon as you conform to Sequence, these standard operations come through protocol extensions. You do not need to implement them yourself.


Frequently asked questions

Q. Is conforming to Sequence enough, or is IteratorProtocol also required? A. An iterator is ultimately required for for-in. However, as in the example above, if one type conforms to both, you do not need to implement makeIterator() separately. If you want to separate iteration state from the collection, extract the iterator into a separate struct.

Q. What happens if you iterate over the same sequence again after using it once in for-in? A. Value types such as arrays create a new iterator each time, so you can iterate over them safely more than once. By contrast, sequences that disappear when consumed, such as network streams, can only be iterated once, so be careful.

Q. How is Collection different? A. Collection is a higher-level concept that inherits from Sequence. It requires more, such as repeated indexed access, counting with count, and preserving order. If you only need to traverse values in one direction, Sequence is enough.

Drawing the relationships between Sequence, Iterator, and Collection made everything click at a glance
Drawing the relationships between Sequence, Iterator, and Collection made everything click at a glance

Behind a single line of for-in lies a clean division of responsibilities between Sequence and IteratorProtocol: the iterator acts as the cursor, and the sequence acts as the factory. Once you distinguish the two, creating custom iterable types becomes much easier.

The next time you use for-in, try thinking, “next() is being called right now.” I recommend building a Fibonacci sequence yourself first.