What are Sequence and IteratorProtocol Protocols?

Sequence and IteratorProtocol are two related protocols that define a common interface for types that provide sequential, iterated access to their elements. A sequence is a list of values that you can step through one at a time, such as an array, a range, or a string. A sequence can also be infinite, such as the sequence of natural numbers or the sequence of Fibonacci numbers.

An iterator is a type that supplies the values of a sequence one at a time. An iterator keeps track of its iteration process and returns one element at a time as it advances through the sequence. Whenever you use a for-in loop with an array, set, or any other collection or sequence, you’re using that type’s iterator. Swift uses a sequence’s or collection’s iterator internally to enable the for-in loop language construct.

The Sequence protocol defines only one requirement: a makeIterator() method that returns an iterator. The IteratorProtocol protocol defines only one requirement: a next() method that returns an optional value. The next() method must return nil after it has exhausted its elements.

The Sequence protocol provides default implementations for many common operations that depend on sequential access to a sequence’s values, such as contains(:), map(:), filter(:), reduce(:_:), and more. These operations are available for any type that conforms to the Sequence protocol.

Why Use Sequence and IteratorProtocol Protocols?

Making your own custom types conform to Sequence and IteratorProtocol protocols enables many useful operations, like for-in looping and the contains() method, without much effort. To add Sequence conformance to your own custom type, you just need to add a makeIterator() method that returns an iterator. Alternatively, if your type can act as its own iterator, implementing the requirements of the IteratorProtocol protocol and declaring conformance to both Sequence and IteratorProtocol protocols are sufficient.

By conforming to Sequence and IteratorProtocol protocols, you can also leverage many powerful features of Swift, such as lazy evaluation, higher-order functions, generics, and protocol extensions. For example, you can use map(), filter(), reduce(), and other methods from the Sequence protocol extension to transform or aggregate your data. You can also use AnySequence and AnyIterator types to erase the type information of your sequences and iterators and make them more generic.

How to Use Sequence and IteratorProtocol Protocols in Swift?

To demonstrate how to use Sequence and IteratorProtocol protocols in Swift, we will create a simple example of a countdown. A countdown is a sequence that starts from a given number and decrements by one until it reaches zero. For example, a countdown from 3 would produce the values 3, 2, 1, 0.

Here is how we can define a generic Countdown struct that represents a countdown from any given number:

struct Countdown<T: Numeric & Comparable>: Sequence, IteratorProtocol {
    // The starting number
    var start: T
    // The current number
    var current: T
    
    // The initializer
    init(from start: T) {
        self.start = start
        self.current = start
    }
    
    // The next() method that returns the next number or nil
    mutating func next() -> T? {
        // If the current number is greater than or equal to zero, return it
        if current >= 0 {
            // Store the current number in a temporary variable
            let temp = current
            // Decrement the current number by one
            current -= 1
            // Return the temporary variable
            return temp
        } else {
            // Otherwise, return nil
            return nil
        }
    }
}

Here we use generics to make our countdown type-agnostic. We also use Numeric and Comparable constraints to ensure that our generic type supports arithmetic operations and comparison operators. We also conform to both Sequence and IteratorProtocol protocols by implementing the next() method.

Now we can create an instance of our countdown and iterate over it using a for-in loop:

// Create an instance of Countdown from 3
let threeToGo = Countdown(from: 3)
// Iterate over the countdown using a for-in loop
for number in threeToGo {
    print(number)
}
// Prints 3
// Prints 2
// Prints 1
// Prints 0

We can also use other methods that depend on sequential access, such as the contains() method:

// Create an instance of Countdown from 5
let fiveToGo = Countdown(from: 5)
// Check if the countdown contains 3
if fiveToGo.contains(3) {
    print("The countdown contains 3")
} else {
    print("The countdown does not contain 3")
}
// Prints "The countdown contains 3"

Conclusion

In this blog post, we have learned about Sequence and IteratorProtocol protocols in Swift. These protocols are useful for accessing the elements of a collection or a sequence in a sequential and iterative way. We have seen how to use them with for-in loops and other methods, and how to make our own custom types conform to them. We hope you enjoyed this blog post and learned something new. Thanks for reading!