Bird
Raised Fist0
Swiftprogramming~15 mins

Sequence protocol for custom iteration in Swift - Deep Dive

Choose your learning style10 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().

Practice

(1/5)
1. What does the Sequence protocol in Swift require you to provide for custom iteration?
easy
A. A property that stores all elements in an array
B. A method named start() that begins the iteration
C. A function that returns the count of elements
D. An iterator with a next() method that returns elements or nil

Solution

  1. Step 1: Understand the Sequence protocol requirement

    The Sequence protocol requires an iterator that can provide elements one by one.
  2. Step 2: Identify the iterator method

    This iterator must have a next() method that returns the next element or nil when done.
  3. Final Answer:

    An iterator with a next() method that returns elements or nil -> Option D
  4. Quick Check:

    Sequence needs next() iterator method = C [OK]
Hint: Sequence needs an iterator with next() returning nil at end [OK]
Common Mistakes:
  • Thinking Sequence requires a start() method
  • Assuming all elements must be stored in an array
  • Confusing count with iteration method
2. Which of the following is the correct way to declare a custom iterator type conforming to IteratorProtocol in Swift?
easy
A. struct MyIterator: IteratorProtocol { func next() -> Int { return 0 } }
B. class MyIterator { func next() -> Int { return 0 } }
C. struct MyIterator: Sequence { func next() -> Int? { return nil } }
D. struct MyIterator: IteratorProtocol { func next() -> Int? { return nil } }

Solution

  1. Step 1: Check protocol conformance and method signature

    IteratorProtocol requires a next() method returning an optional element type, here Int?.
  2. Step 2: Validate each option

    struct MyIterator: IteratorProtocol { func next() -> Int? { return nil } } correctly declares a struct conforming to IteratorProtocol with next() -> Int?. class MyIterator { func next() -> Int { return 0 } } lacks conformance and returns non-optional. struct MyIterator: Sequence { func next() -> Int? { return nil } } wrongly conforms to Sequence, not IteratorProtocol. struct MyIterator: IteratorProtocol { func next() -> Int { return 0 } } returns non-optional Int, which is incorrect.
  3. Final Answer:

    struct MyIterator: IteratorProtocol { func next() -> Int? { return nil } } -> Option A
  4. Quick Check:

    IteratorProtocol requires next() returning optional = D [OK]
Hint: IteratorProtocol's next() must return optional element type [OK]
Common Mistakes:
  • Returning non-optional from next()
  • Confusing Sequence with IteratorProtocol
  • Missing protocol conformance declaration
3. Consider this Swift code implementing a custom sequence:
struct CountDown: Sequence {
  let start: Int
  func makeIterator() -> some IteratorProtocol {
    var current = start
    return AnyIterator {
      if current <= 0 { return nil }
      defer { current -= 1 }
      return current
    }
  }
}

for number in CountDown(start: 3) {
  print(number)
}
What is the output?
medium
A. 3 2 1 0
B. 3 2 1
C. 4 3 2 1
D. 1 2 3

Solution

  1. Step 1: Understand the iterator logic

    The iterator starts at start (3). It returns current if > 0, then decreases current by 1 after returning.
  2. Step 2: Trace the iteration values

    First call returns 3, then current becomes 2. Next returns 2, current becomes 1. Next returns 1, current becomes 0. Next call returns nil because current is 0, ending iteration.
  3. Final Answer:

    3 2 1 -> Option B
  4. Quick Check:

    CountDown from 3 to 1 = 3 2 1 [OK]
Hint: Check when next() returns nil to find iteration end [OK]
Common Mistakes:
  • Including 0 in output though iteration stops before
  • Assuming count goes up instead of down
  • Misreading defer behavior
4. This code attempts to create a custom sequence but has an error:
struct MySequence: Sequence {
  func makeIterator() -> MyIterator {
    return MyIterator()
  }
}

struct MyIterator: IteratorProtocol {
  var count = 3
  func next() -> Int? {
    if count == 0 { return nil }
    count -= 1
    return count
  }
}
What is the main problem here?
medium
A. next() is a func but modifies count, which is a struct property
B. makeIterator() should return IteratorProtocol instead of MyIterator
C. count should start at 0, not 3
D. next() must return non-optional Int

Solution

  1. Step 1: Identify mutability issue in next()

    The next() method tries to change count, but it is declared in a struct and next() is not marked mutating.
  2. Step 2: Understand Swift struct rules

    In structs, methods that modify properties must be marked mutating. Without it, the compiler will error.
  3. Final Answer:

    next() is a func but modifies count, which is a struct property -> Option A
  4. Quick Check:

    Mutating methods must be marked mutating = A [OK]
Hint: Mark methods mutating if they change struct properties [OK]
Common Mistakes:
  • Forgetting mutating keyword on next()
  • Thinking return type must be non-optional
  • Confusing return type with protocol requirement
5. You want to create a custom sequence that iterates over only even numbers from 2 up to a given limit. Which implementation correctly uses the Sequence protocol for this?
hard
A. struct EvenNumbers: Sequence { let limit: Int func makeIterator() -> AnyIterator<Int> { var current = 2 return AnyIterator { while current <= limit { let result = current current += 2 return result } return nil } } }
B. struct EvenNumbers: Sequence { let limit: Int func makeIterator() -> AnyIterator<Int> { var current = 0 return AnyIterator { current += 2 if current < limit { return current } return nil } } }
C. struct EvenNumbers: Sequence { let limit: Int func makeIterator() -> AnyIterator<Int> { var current = 2 return AnyIterator { if current <= limit { let result = current current += 2 return result } return nil } } }
D. struct EvenNumbers: Sequence { let limit: Int func makeIterator() -> AnyIterator<Int> { var current = 0 return AnyIterator { if current < limit { current += 2 return current } return nil } } }

Solution

  1. Step 1: Check iterator logic for even numbers

    The iterator should start at 2 and return current if <= limit, then add 2 for next call.
  2. Step 2: Analyze each option's iteration behavior

    struct EvenNumbers: Sequence { let limit: Int func makeIterator() -> AnyIterator<Int> { var current = 2 return AnyIterator { while current <= limit { let result = current current += 2 return result } return nil } } } uses a while loop inside the closure, which causes it to return only once and then exit, so it won't iterate properly. struct EvenNumbers: Sequence { let limit: Int func makeIterator() -> AnyIterator<Int> { var current = 2 return AnyIterator { if current <= limit { let result = current current += 2 return result } return nil } } } uses an if check and returns current then increments, correctly producing even numbers up to limit. struct EvenNumbers: Sequence { let limit: Int func makeIterator() -> AnyIterator<Int> { var current = 0 return AnyIterator { current += 2 if current < limit { return current } return nil } } } increments before checking, so first returned number is 2, which is correct, but starts at 0 which is okay. struct EvenNumbers: Sequence { let limit: Int func makeIterator() -> AnyIterator<Int> { var current = 0 return AnyIterator { if current < limit { current += 2 return current } return nil } } } increments after checking current < limit, so it may skip the limit if limit is even. struct EvenNumbers: Sequence { let limit: Int func makeIterator() -> AnyIterator<Int> { var current = 2 return AnyIterator { if current <= limit { let result = current current += 2 return result } return nil } } } is the clearest and correct implementation.
  3. Final Answer:

    struct EvenNumbers: Sequence { let limit: Int func makeIterator() -> AnyIterator<Int> { var current = 2 return AnyIterator { if current <= limit { let result = current current += 2 return result } return nil } } } -> Option C
  4. Quick Check:

    Use if check and increment after return for correct iteration = B [OK]
Hint: Use if condition inside iterator closure, not while loop [OK]
Common Mistakes:
  • Using while loop inside iterator closure causing single return
  • Incrementing before returning causing off-by-one errors
  • Incorrect start value causing wrong first element
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard