Printed on: November 5, 2025
While you write for merchandise in record the compiler quietly units numerous equipment in movement. Normally writing a for loop is a fairly mundane job, it isn’t that advanced of a syntax to jot down. Nevertheless, it is all the time enjoyable to dig a bit deeper and see what occurs underneath the hood. On this submit I’ll unpack the items that make iteration tick so you may cause about loops with the identical confidence you have already got round optionals, enums, or end result builders.
Right here’s what you’ll decide up:
- What
SequenceandAssortmentpromise—and why iterators are nearly all the time structs. - How
for … indesugars, plus the pitfalls of mutating when you loop. - How async iteration and customized collections lengthen the identical core concepts.
Understanding Sequence
Sequence is the smallest unit of iteration in Swift and it comes with a really intentional contract: “when any individual asks for an iterator, give them one that may hand out components till you’re out”. Which means a conforming kind must outline two related varieties (Factor and Iterator) and return a contemporary iterator each time makeIterator() known as.
public protocol Sequence {
associatedtype Factor
associatedtype Iterator: IteratorProtocol the place Iterator.Factor == Factor
func makeIterator() -> Iterator
}
The iterator itself conforms to IteratorProtocol and exposes a mutating subsequent() operate:
public protocol IteratorProtocol {
associatedtype Factor
mutating func subsequent() -> Factor?
}
You’ll see most iterators applied as structs. subsequent() is marked mutating, so a value-type iterator can replace its place with none further ceremony. While you copy the iterator, you get a contemporary cursor that resumes from the identical level, which retains iteration predictable and prevents shared mutable state from leaking between loops. Courses can undertake IteratorProtocol too, however worth semantics are a pure match for the contract.
There are two essential implications to bear in mind:
- A sequence solely needs to be single-pass. It’s completely legitimate at hand out a “consumable” iterator that can be utilized as soon as after which returns
nileternally. Lazy I/O streams or generator-style APIs lean on this behaviour. makeIterator()ought to produce a contemporary iterator every time you name it. Some sequences select to retailer and reuse an iterator internally, however the contract encourages the “new iterator per loop” mannequin soforloops can run independently with out odd interactions.
In case you’ve ever used stride(from:to:by:) you’ve already labored with a plain Sequence. The usual library exposes it proper subsequent to ranges, and it’s excellent for strolling an arithmetic development with out allocating an array. For instance:
for angle in stride(from: 0, by: 360, by: 30) {
print(angle)
}
This prints 0, 30, 60 … 360 after which the iterator is completed. In case you ask for an additional iterator you’ll get a brand new run, however there’s no requirement that the unique one resets itself or that the sequence shops all of its values. It simply retains the present step and palms out the following quantity till it reaches the tip. That’s the core Sequence contract in motion.
So to summarize, a Sequence accommodates n gadgets (we do not know what number of as a result of there isn’t any idea of depend in a Sequence), and we are able to ask the Sequence for an Iterator to obtain gadgets till the Sequence runs out. As you noticed with stride, the Sequence does not have to carry all values it’ll ship in reminiscence. It may possibly generate the values each time its Iterator has its subsequent() operate known as.
In case you want a number of passes, random entry, or counting, Sequence received’t offer you that by itself. The protocol doesn’t forbid throwing the weather away after the primary cross; AsyncStream-style sequences do precisely that. An AsyncStream will vend a brand new worth to an async loop, after which it discards the worth eternally.
In different phrases, the one promise is “I can vend an iterator”. Nothing says the iterator might be rewound or that calling makeIterator() twice produces the identical outcomes. That’s the place Assortment steps in.
Assortment’s Further Ensures
Assortment refines Sequence with the guarantees we lean on day-to-day: you may iterate as many instances as you want, the order is secure (so long as the gathering’s personal documentation says so), and also you get indexes, subscripts, and counts. Swift’s Array, Dictionary, and Set all conform to the Assortment protocol for instance.
public protocol Assortment: Sequence {
associatedtype Index: Comparable
var startIndex: Index { get }
var endIndex: Index { get }
func index(after i: Index) -> Index
subscript(place: Index) -> Factor { get }
}
These further necessities unlock optimisations. map can preallocate precisely the correct quantity of storage. depend doesn’t have to stroll all the knowledge set. If a Assortment additionally implements BidirectionalCollection or RandomAccessCollection the compiler can apply much more optimizations at no cost.
Value noting: Set and Dictionary each conform to Assortment despite the fact that their order can change after you mutate them. The protocols don’t promise order, so if iteration order issues to you be sure you decide a kind that paperwork the way it behaves.
How for … in Really Works
Now that you realize a bit extra about collections and iterating them in Swift, right here’s what a easy loop appears to be like like in case you have been to jot down one with out utilizing for x in y:
var iterator = container.makeIterator()
whereas let factor = iterator.subsequent() {
print(factor)
}
To make this concrete, right here’s a small customized sequence that may depend down from a given beginning quantity:
struct Countdown: Sequence {
let begin: Int
func makeIterator() -> Iterator {
Iterator(present: begin)
}
struct Iterator: IteratorProtocol {
var present: Int
mutating func subsequent() -> Int? {
guard present >= 0 else { return nil }
defer { present -= 1 }
return present
}
}
}
Operating for quantity in Countdown(begin: 3) executes the desugared loop above. Copy the iterator midway by and every copy continues independently due to worth semantics.
One factor to keep away from: mutating the underlying storage when you’re in the course of iterating it. An array iterator assumes the buffer stays secure; in case you take away a component, the buffer shifts and the iterator now not is aware of the place the following factor lives, so the runtime traps with Assortment modified whereas enumerating. When it’s essential to cull gadgets, there are safer approaches: name removeAll(the place:) which handles the iteration for you, seize the indexes first and mutate after the loop, or construct a filtered copy and change the unique when you’re executed.
Right here’s what an actual bug appears to be like like. Think about a listing of duties the place you need to strip the finished ones:
struct TodoItem {
var title: String
var isCompleted: Bool
}
var todoItems = [
TodoItem(title: "Ship blog post", isCompleted: true),
TodoItem(title: "Record podcast", isCompleted: false),
TodoItem(title: "Review PR", isCompleted: true),
]
for merchandise in todoItems {
if merchandise.isCompleted,
let index = todoItems.firstIndex(the place: { $0.title == merchandise.title }) {
todoItems.take away(at: index) // ⚠️ Deadly error: Assortment modified whereas enumerating.
}
}
Operating this code crashes the second the primary accomplished job is eliminated as a result of the iterator nonetheless expects the previous format. It additionally calls firstIndex on each cross, so every iteration scans the entire array once more—a simple approach to flip a fast cleanup into O(n²) work. A safer rewrite delegates the traversal:
todoItems.removeAll(the place: .isCompleted)
As a result of removeAll(the place:) owns the traversal, it walks the array as soon as and removes matches in place.
In case you choose to maintain the originals round, construct a filtered copy as an alternative:
let openTodos = todoItems.filter { !$0.isCompleted }
Each approaches hold iteration and mutation separated, which suggests you received’t journey over the iterator mid-loop. All the things we’ve checked out up to now assumes the weather are prepared the second you ask for them. In fashionable apps, it isn’t unusual to need to iterate over collections (or streams) that generate new values over time. Swift’s concurrency options lengthen the very same iteration patterns into that world.
Async Iteration in Observe
Swift Concurrency introduces AsyncSequence and AsyncIteratorProtocol. These look acquainted, however the iterator’s subsequent() technique can droop and throw.
public protocol AsyncSequence {
associatedtype Factor
associatedtype AsyncIterator: AsyncIteratorProtocol the place AsyncIterator.Factor == Factor
func makeAsyncIterator() -> AsyncIterator
}
public protocol AsyncIteratorProtocol {
associatedtype Factor
mutating func subsequent() async throws -> Factor?
}
You devour async sequences with for await:
for await factor in stream {
print(factor)
}
Underneath the hood the compiler builds a looping job that repeatedly awaits subsequent(). If subsequent() can throw, swap to for strive await. Errors propagate identical to they might in every other async context.
Most callback-style APIs might be bridged with AsyncStream. Right here’s a condensed instance that publishes progress updates:
func makeProgressStream() -> AsyncStream {
AsyncStream { continuation in
let token = progressManager.observe { fraction in
continuation.yield(fraction)
if fraction == 1 { continuation.end() }
}
continuation.onTermination = { _ in
progressManager.removeObserver(token)
}
}
}
for await fraction in makeProgressStream() now suspends between values. Don’t overlook to name end() once you’re executed producing output, in any other case downstream loops by no means exit.
Since async loops run inside duties, they need to play properly with cancellation. The simplest sample is to verify for cancellation inside subsequent():
struct PollingIterator: AsyncIteratorProtocol {
mutating func subsequent() async throws -> Merchandise? {
strive Job.checkCancellation()
return await fetchNextItem()
}
}
If the duty is cancelled you’ll see CancellationError, which ends the loop routinely except you resolve to catch it.
Implementing your personal collections
Most of us by no means must construct a set from scratch—and that’s factor. Arrays, dictionaries, and units already cowl the vast majority of circumstances with battle-tested semantics. While you do roll your personal, tread rigorously: you’re promising index validity, multi-pass iteration, efficiency traits, and all the opposite traits that callers anticipate from the usual library. A tiny mistake can corrupt indices or put you in undefined territory.
Nonetheless, there are reputable causes to create a specialised assortment. You may want a hoop buffer that overwrites previous entries, or a sliding window that exposes simply sufficient knowledge for a streaming algorithm. Everytime you go down this path, hold the floor space tight, doc the invariants, and write exhaustive exams to show the gathering acts like a typical one.
Even so, it is price exploring a customized implementation of Assortment for the sake of finding out it. Right here’s a light-weight ring buffer that conforms to Assortment:
struct RingBuffer: Assortment {
non-public var storage: [Element?]
non-public var head = 0
non-public var tail = 0
non-public(set) var depend = 0
init(capability: Int) {
storage = Array(repeating: nil, depend: capability)
}
mutating func enqueue(_ factor: Factor) {
storage[tail] = factor
tail = (tail + 1) % storage.depend
if depend == storage.depend {
head = (head + 1) % storage.depend
} else {
depend += 1
}
}
// MARK: Assortment
typealias Index = Int
var startIndex: Int { 0 }
var endIndex: Int { depend }
func index(after i: Int) -> Int {
precondition(i Factor {
precondition((0..
A couple of particulars in that snippet are price highlighting:
storageshops optionals so the buffer can hold a set capability whereas monitoring empty slots.headandtailadvance as you enqueue, however the array by no means reallocates.dependis maintained individually. A hoop buffer is likely to be partially stuffed, so counting onstorage.dependwould lie about what number of components are literally obtainable.index(after:)and the subscript settle for logical indexes (0 bydepend) and translate them to the appropriate slot instorageby offsetting fromheadand wrapping with the modulo operator. That bookkeeping retains iteration secure even after the buffer wraps round.- Every accessor defends the invariants with
precondition. Skip these checks and a stray index can pull stale knowledge or stroll off the tip with out warning.
Even in an instance as small because the one above, you may see how a lot accountability you tackle when you undertake Assortment.
In Abstract
Iteration appears to be like easy as a result of Swift hides the boilerplate, however there’s a surprisingly wealthy protocol hierarchy behind each loop. As soon as you understand how Sequence, Assortment, and their async siblings work together, you may construct knowledge constructions that really feel pure in Swift, cause about efficiency, and bridge legacy callbacks into clear async code.
If you wish to hold exploring after this, revisit the posts I’ve written on actors and knowledge races to see how iteration interacts with isolation. Or take one other take a look at my items on map and flatMap to dig deeper into lazy sequences and useful pipelines. Both means, the following time you attain for for merchandise in record, you’ll know precisely what’s occurring underneath the hood and the way to decide on the appropriate method for the job.

