0
0
Swiftprogramming~15 mins

Sequence protocol for custom iteration in Swift - Deep Dive

Choose your learning style9 modes available
Overview - Sequence protocol for custom iteration
What is it?
The Sequence protocol in Swift defines a way to create a custom type that can be iterated over using a loop. It requires the type to provide an iterator, which produces elements one by one. This lets you use your custom collection or data structure in for-in loops and other iteration contexts.
Why it matters
Without the Sequence protocol, you would have to manually manage how to access each element in your custom data types, making code repetitive and error-prone. The protocol standardizes iteration, so Swift can work with any sequence in a consistent way, improving code reuse and readability.
Where it fits
Before learning Sequence, you should understand basic Swift types, loops, and functions. After mastering Sequence, you can explore Collection protocol for more advanced features like indexing and slicing, or learn about lazy sequences and functional programming patterns.
Mental Model
Core Idea
A Sequence is a type that knows how to hand out its elements one at a time through an iterator.
Think of it like...
Imagine a vending machine that gives you one snack each time you press a button. The Sequence is the vending machine, and the iterator is the button you press repeatedly to get the next snack.
Sequence
  │
  ├─ makeIterator() → Iterator
  │                   │
  │                   ├─ next() → Element?
  │                   └─ next() → Element?
  └─ for element in sequence { ... }
Build-Up - 6 Steps
1
FoundationUnderstanding the Sequence protocol basics
🤔
Concept: Sequence requires a method to create an iterator that produces elements one by one.
In Swift, a type conforms to Sequence by implementing the makeIterator() method. This method returns an iterator that conforms to IteratorProtocol, which has a next() method returning the next element or nil when done. Example: struct CountToThree: Sequence { func makeIterator() -> some IteratorProtocol { return CountToThreeIterator() } } struct CountToThreeIterator: IteratorProtocol { var count = 1 mutating func next() -> Int? { if count <= 3 { defer { count += 1 } return count } else { return nil } } }
Result
You can now use CountToThree in a for-in loop to get numbers 1, 2, and 3.
Understanding that Sequence only requires a way to produce an iterator clarifies how iteration is separated from the collection itself.
2
FoundationIteratorProtocol and the next() method
🤔
Concept: The iterator controls the actual iteration by returning elements one at a time or nil when finished.
IteratorProtocol requires a mutating next() method that returns the next element or nil if there are no more. Example: struct FibonacciIterator: IteratorProtocol { var state = (0, 1) mutating func next() -> Int? { let nextValue = state.0 state = (state.1, state.0 + state.1) return nextValue } }
Result
Calling next() repeatedly on FibonacciIterator produces the Fibonacci sequence indefinitely.
Knowing that the iterator holds the iteration state explains why next() must be mutating and how iteration progresses.
3
IntermediateCreating a custom Sequence with internal state
🤔Before reading on: do you think the Sequence itself holds iteration state or the iterator does? Commit to your answer.
Concept: The Sequence type itself does not hold iteration state; the iterator does, allowing multiple independent iterations.
When you create a Sequence, it should produce a fresh iterator each time makeIterator() is called. This means you can iterate multiple times independently. Example: struct Countdown: Sequence { let start: Int func makeIterator() -> CountdownIterator { return CountdownIterator(current: start) } } struct CountdownIterator: IteratorProtocol { var current: Int mutating func next() -> Int? { if current >= 0 { defer { current -= 1 } return current } else { return nil } } }
Result
Each for-in loop over Countdown starts fresh from the start value.
Understanding that iteration state belongs to the iterator prevents bugs where multiple loops interfere with each other.
4
IntermediateUsing Sequence with for-in loops and standard functions
🤔Before reading on: do you think all standard Swift functions like map and filter work on any Sequence? Commit to your answer.
Concept: Sequence types can be used with for-in loops and many standard library functions like map, filter, and reduce.
Once your type conforms to Sequence, you can use it naturally: let countdown = Countdown(start: 3) for number in countdown { print(number) } let doubled = countdown.map { $0 * 2 } print(doubled) These functions work because they accept any Sequence as input.
Result
You get printed numbers 3, 2, 1, 0 and an array [6, 4, 2, 0] from map.
Knowing that conforming to Sequence unlocks powerful, reusable iteration tools encourages designing custom types this way.
5
AdvancedImplementing lazy sequences for efficiency
🤔Before reading on: do you think Sequence always computes all elements upfront? Commit to your answer.
Concept: Sequences can be lazy, producing elements only when needed, which saves memory and computation.
By default, Sequence produces elements on demand via the iterator. You can chain lazy operations to avoid creating intermediate collections. Example: let lazySquares = countdown.lazy.map { $0 * $0 } for square in lazySquares { print(square) } This computes squares only as the loop runs, not all at once.
Result
Squares 9, 4, 1, 0 are printed one by one without extra memory use.
Understanding laziness helps write efficient code that handles large or infinite sequences gracefully.
6
ExpertCustom Sequence pitfalls and iterator reuse traps
🤔Before reading on: do you think reusing the same iterator instance for multiple loops is safe? Commit to your answer.
Concept: Reusing the same iterator instance across loops breaks iteration because iterator state is mutable and single-use.
If makeIterator() returns the same iterator object every time, loops interfere: class BadSequence: Sequence { var iterator = CountdownIterator(current: 3) func makeIterator() -> CountdownIterator { return iterator } } Using BadSequence in two loops causes unexpected results because iterator state is shared.
Result
Second loop may produce no elements or partial results due to exhausted iterator.
Knowing that each iteration needs a fresh iterator prevents subtle bugs in custom sequences.
Under the Hood
When you use a for-in loop on a Sequence, Swift calls makeIterator() to get an iterator. Then it repeatedly calls next() on the iterator to get elements until next() returns nil. The iterator keeps track of where it is in the sequence internally, often using mutable state. This separation allows multiple independent iterations over the same sequence.
Why designed this way?
Swift separates Sequence and IteratorProtocol to allow flexible iteration patterns. The Sequence is a blueprint for iteration, while the iterator is the actual cursor moving through elements. This design supports multiple simultaneous iterations and lazy evaluation. Earlier languages often combined these roles, limiting flexibility.
Sequence
  │
  ├─ makeIterator() ──▶ Iterator
  │                      │
  │                      ├─ next() → Element or nil
  │                      ├─ next() → Element or nil
  │                      └─ ...
  └─ for element in sequence { ... } calls makeIterator() once per loop
Myth Busters - 4 Common Misconceptions
Quick: Does the Sequence itself hold iteration state or the iterator? Commit to your answer.
Common Belief:The Sequence holds the current position of iteration.
Tap to reveal reality
Reality:The iterator holds the iteration state; the Sequence only creates new iterators.
Why it matters:If you mistakenly store iteration state in the Sequence, multiple loops can interfere and produce wrong results.
Quick: Can you use the same iterator instance for multiple loops safely? Commit to your answer.
Common Belief:You can reuse the same iterator instance for multiple iterations.
Tap to reveal reality
Reality:Iterators are single-use and mutable; reusing them causes loops to fail or produce incomplete data.
Why it matters:Reusing iterators leads to bugs that are hard to detect because loops silently produce no or partial output.
Quick: Does conforming to Sequence mean your type must store all elements in memory? Commit to your answer.
Common Belief:Sequence types must store all elements in memory before iteration.
Tap to reveal reality
Reality:Sequences can be lazy and generate elements on demand without storing them all.
Why it matters:Assuming eager storage can lead to inefficient code and prevent handling large or infinite sequences.
Quick: Does conforming to Sequence automatically make your type a Collection? Commit to your answer.
Common Belief:Sequence and Collection are the same; conforming to Sequence means you have all Collection features.
Tap to reveal reality
Reality:Sequence only supports simple iteration; Collection adds indexing, count, and more complex features.
Why it matters:Confusing these leads to using unavailable features and runtime errors.
Expert Zone
1
makeIterator() must return a fresh iterator each time to support multiple independent loops.
2
IteratorProtocol's next() is mutating because it changes the iterator's internal state to track progress.
3
Sequences can be infinite, so iteration must be carefully controlled to avoid infinite loops or resource exhaustion.
When NOT to use
Use Sequence when you only need simple iteration. For random access, slicing, or efficient indexing, use Collection instead. If you need thread-safe iteration or concurrent access, consider specialized concurrency-safe collections.
Production Patterns
In production, custom sequences often wrap external data sources like files or network streams to provide lazy, memory-efficient iteration. They are also used to create domain-specific data pipelines that integrate seamlessly with Swift's standard library functions.
Connections
Iterator pattern (software design)
Sequence and IteratorProtocol implement the Iterator design pattern in Swift.
Understanding the Iterator pattern from software design helps grasp why Swift separates Sequence and IteratorProtocol for flexible iteration.
Generators in Python
Generators provide a similar lazy iteration mechanism as Swift's Sequence and IteratorProtocol.
Knowing Python generators helps understand how Swift sequences produce elements on demand without storing all data.
Conveyor belt systems (manufacturing)
Like a conveyor belt delivering items one by one, a Sequence hands out elements sequentially through its iterator.
This connection shows how iteration is a controlled, step-by-step delivery process, not a bulk transfer.
Common Pitfalls
#1Sharing the same iterator instance for multiple loops.
Wrong approach:class BadSequence: Sequence { var iterator = CountdownIterator(current: 3) func makeIterator() -> CountdownIterator { return iterator } }
Correct approach:struct GoodSequence: Sequence { let start: Int func makeIterator() -> CountdownIterator { return CountdownIterator(current: start) } }
Root cause:Misunderstanding that iterators are single-use and must be fresh for each iteration.
#2Storing iteration state inside the Sequence instead of the iterator.
Wrong approach:struct WrongSequence: Sequence { var current = 0 func makeIterator() -> some IteratorProtocol { return self } mutating func next() -> Int? { if current < 3 { defer { current += 1 } return current } else { return nil } } }
Correct approach:struct RightSequence: Sequence { func makeIterator() -> CountdownIterator { return CountdownIterator(current: 0) } } struct CountdownIterator: IteratorProtocol { var current: Int mutating func next() -> Int? { if current < 3 { defer { current += 1 } return current } else { return nil } } }
Root cause:Confusing the roles of Sequence and IteratorProtocol leads to incorrect state management.
#3Assuming Sequence requires storing all elements eagerly.
Wrong approach:struct EagerSequence: Sequence { let elements = [1, 2, 3] func makeIterator() -> IndexingIterator<[Int]> { return elements.makeIterator() } }
Correct approach:struct LazySequence: Sequence { func makeIterator() -> some IteratorProtocol { var count = 1 return AnyIterator { guard count <= 3 else { return nil } defer { count += 1 } return count } } }
Root cause:Not realizing that Sequence can generate elements on demand without storing them all.
Key Takeaways
The Sequence protocol defines a way to iterate over custom types by providing an iterator that produces elements one at a time.
Iteration state belongs to the iterator, not the Sequence, allowing multiple independent loops over the same sequence.
Conforming to Sequence unlocks powerful standard library functions like map, filter, and reduce for your custom types.
Sequences can be lazy, generating elements only when needed, which improves efficiency and supports infinite sequences.
Misusing iterators by sharing or reusing them causes subtle bugs; always return a fresh iterator from makeIterator().