Skip to Content
Course content

64: The Sequence and IteratorProtocol

Click on the "Edit" button in the top corner of the screen to edit your slide content.

One of the most confusing parts of the Swift standard library is the distinction between a Sequence and an IteratorProtocol. On the surface, they both seem to be about "getting a list of things," but they serve fundamentally different purposes. I see developers trip over this constantly, usually when they try to build a custom collection or a data streamer.

struct Countdown: Sequence, IteratorProtocol {
    var value: Int
    
    init(start: Int) {
        self.value = start
    }
    
    mutating func next() -> Int? {
        if value <= 0 { return nil }
        defer { value -= 1 }
        return value
    }
}

var timer = Countdown(start: 3)

// First loop: Works great!
for num in timer {
    print(num) // Prints 3, 2, 1
}

// Second loop: Does absolutely nothing.
for num in timer {
    print(num) // Silence...
}

The One-Shot Iteration Bug

If you run the code above, you'll notice the first for-in loop works exactly as expected. But the second one is a ghost town. Why? Because we made the Countdown struct conform to both Sequence and IteratorProtocol at the same time.

In Swift, an IteratorProtocol is stateful. The next() method mutates the iterator to keep track of where it is in the sequence. By making Countdown its own iterator, we've baked the "current position" state directly into the sequence itself. Once the first loop finishes, value has been decremented to 0. When the second loop starts, it asks the sequence for an iterator, gets itself, and immediately finds that next() returns nil. The sequence is "exhausted."

Separating State from the Sequence

To fix this, we need to separate the definition of the sequence from the process of iterating over it. The Sequence should be a factory that can produce a fresh IteratorProtocol every time it's needed. I usually do this by creating a small, private nested struct for the iterator.

struct Countdown: Sequence {
    let start: Int
    
    // This is the "factory" method required by Sequence
    func makeIterator() -> CountdownIterator {
        return CountdownIterator(start: start)
    }
    
    struct CountdownIterator: IteratorProtocol {
        var current: Int
        
        init(start: Int) {
            self.current = start
        }
        
        mutating func next() -> Int? {
            if current <= 0 { return nil }
            defer { current -= 1 }
            return current
        }
    }
}

let timer = Countdown(start: 3)

for num in timer {
    print(num) // Prints 3, 2, 1
}

for num in timer {
    print(num) // Now this also prints 3, 2, 1!
}

How the Handshake Works

When you write for num in timer, Swift doesn't actually call next() on the sequence. Instead, it calls makeIterator(). This returns a brand new instance of CountdownIterator. Because the state (the current variable) lives inside that temporary iterator and not in the Countdown struct, we can iterate over the same sequence as many times as we want.

You might wonder: "Why not just make everything a Sequence?" Because some things are meant to be one-time streams. For example, a network socket reading bytes is a perfect candidate for a single-use iterator. But for data structures, you almost always want the factory pattern I showed above. If you find yourself adding mutating to your Sequence methods, stop and ask yourself if you're accidentally treating your sequence like an iterator.




📋 Practical Task

Implementing a Fibonacci Sequence Generator

Your task is to create a custom Sequence that generates Fibonacci numbers. Instead of pre-calculating an array (which would defeat the purpose of a sequence), you must implement the logic within a custom iterator so that numbers are calculated on-the-fly.

Requirements:

  • Create a struct named Fibonacci that conforms to Sequence.
  • The Fibonacci struct should take a limit integer in its initializer, representing the maximum value the sequence should reach.
  • Implement a nested IteratorProtocol that calculates the next Fibonacci number in the sequence (starting from 0 and 1).
  • The next() method should return nil once the next Fibonacci number exceeds the limit.
  • Verify that you can iterate over the same Fibonacci instance twice and get the same results both times.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.