Revealed on: November 5, 2025
Whenever you write for merchandise in checklist the compiler quietly units quite a lot of equipment in movement. Normally writing a for loop is a fairly mundane job, it isn’t that complicated of a syntax to jot down. Nonetheless, it is at all times enjoyable to dig a bit deeper and see what occurs beneath the hood. On this put up I’ll unpack the items that make iteration tick so you possibly can purpose about loops with the identical confidence you have already got round optionals, enums, or outcome builders.
Right here’s what you’ll decide up:
- What
SequenceandAssortmentpromise—and why iterators are nearly at all times 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 someone asks for an iterator, give them one that may hand out components till you’re out”. Meaning a conforming sort must outline two related varieties (Aspect and Iterator) and return a recent iterator each time makeIterator() is named.
public protocol Sequence {
associatedtype Aspect
associatedtype Iterator: IteratorProtocol the place Iterator.Aspect == Aspect
func makeIterator() -> Iterator
}
The iterator itself conforms to IteratorProtocol and exposes a mutating subsequent() perform:
public protocol IteratorProtocol {
associatedtype Aspect
mutating func subsequent() -> Aspect?
}
You’ll see most iterators carried out as structs. subsequent() is marked mutating, so a value-type iterator can replace its place with none additional ceremony. Whenever you copy the iterator, you get a recent 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 necessary implications to bear in mind:
- A sequence solely must be single-pass. It’s completely legitimate handy out a “consumable” iterator that can be utilized as soon as after which returns
nilendlessly. Lazy I/O streams or generator-style APIs lean on this behaviour. makeIterator()ought to produce a recent 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 good 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 finished. 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 fingers out the subsequent quantity till it reaches the tip. That’s the core Sequence contract in motion.
So to summarize, a Sequence incorporates n gadgets (we do not know what number of as a result of there is not any idea of rely in a Sequence), and we will ask the Sequence for an Iterator to obtain gadgets till the Sequence runs out. As you noticed with stride, the Sequence would not have to carry all values it’ll ship in reminiscence. It might generate the values each time its Iterator has its subsequent() perform referred to as.
In case you want a number of passes, random entry, or counting, Sequence gained’t provide you with that by itself. The protocol doesn’t forbid throwing the weather away after the primary move; AsyncStream-style sequences do precisely that. An AsyncStream will vend a brand new worth to an async loop, after which it discards the worth endlessly.
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 Additional Ensures
Assortment refines Sequence with the guarantees we lean on day-to-day: you possibly can 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) -> Aspect { get }
}
These additional necessities unlock optimisations. map can preallocate precisely the correct quantity of storage. rely doesn’t have to stroll the complete 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 ensure you decide a sort that paperwork the way it behaves.
How for … in Really Works
Now that you already know a bit extra about collections and iterating them in Swift, right here’s what a easy loop appears like should you have been to jot down one with out utilizing for x in y:
var iterator = container.makeIterator()
whereas let aspect = iterator.subsequent() {
print(aspect)
}
To make this concrete, right here’s a small customized sequence that can rely 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
}
}
}
Working 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; should you take away a component, the buffer shifts and the iterator not is aware of the place the subsequent aspect lives, so the runtime traps with Assortment modified whereas enumerating. When that you must 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 exchange the unique when you’re completed.
Right here’s what an actual bug appears like. Think about an inventory of duties the place you wish 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.
}
}
Working 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 move, so every iteration scans the entire array once more—a straightforward strategy 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 want to maintain the originals round, construct a filtered copy as a substitute:
let openTodos = todoItems.filter { !$0.isCompleted }
Each approaches maintain iteration and mutation separated, which suggests you gained’t journey over the iterator mid-loop. The whole lot 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 wish 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 Follow
Swift Concurrency introduces AsyncSequence and AsyncIteratorProtocol. These look acquainted, however the iterator’s subsequent() methodology can droop and throw.
public protocol AsyncSequence {
associatedtype Aspect
associatedtype AsyncIterator: AsyncIteratorProtocol the place AsyncIterator.Aspect == Aspect
func makeAsyncIterator() -> AsyncIterator
}
public protocol AsyncIteratorProtocol {
associatedtype Aspect
mutating func subsequent() async throws -> Aspect?
}
You eat async sequences with for await:
for await aspect in stream {
print(aspect)
}
Beneath the hood the compiler builds a looping job that repeatedly awaits subsequent(). If subsequent() can throw, swap to for attempt await. Errors propagate identical to they’d in some 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 neglect to name end() if you’re completed 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? {
attempt Activity.checkCancellation()
return await fetchNextItem()
}
}
If the duty is cancelled you’ll see CancellationError, which ends the loop mechanically except you resolve to catch it.
Implementing your personal collections
Most of us by no means should construct a group from scratch—and that’s a great factor. Arrays, dictionaries, and units already cowl the vast majority of instances with battle-tested semantics. Whenever you do roll your personal, tread fastidiously: you’re promising index validity, multi-pass iteration, efficiency traits, and all the opposite traits that callers count on from the usual library. A tiny mistake can corrupt indices or put you in undefined territory.
Nonetheless, there are professional 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, maintain the floor space tight, doc the invariants, and write exhaustive checks to show the gathering acts like a typical one.
Even so, it is price exploring a customized implementation of Assortment for the sake of learning it. Right here’s a light-weight ring buffer that conforms to Assortment:
struct RingBuffer: Assortment {
personal var storage: [Element?]
personal var head = 0
personal var tail = 0
personal(set) var rely = 0
init(capability: Int) {
storage = Array(repeating: nil, rely: capability)
}
mutating func enqueue(_ aspect: Aspect) {
storage[tail] = aspect
tail = (tail + 1) % storage.rely
if rely == storage.rely {
head = (head + 1) % storage.rely
} else {
rely += 1
}
}
// MARK: Assortment
typealias Index = Int
var startIndex: Int { 0 }
var endIndex: Int { rely }
func index(after i: Int) -> Int {
precondition(i < endIndex, "Can't advance previous endIndex")
return i + 1
}
subscript(place: Int) -> Aspect {
precondition((0..
A number of particulars in that snippet are price highlighting:
storageshops optionals so the buffer can maintain a hard and fast capability whereas monitoring empty slots.headandtailadvance as you enqueue, however the array by no means reallocates.relyis maintained individually. A hoop buffer could be partially crammed, so counting onstorage.relywould lie about what number of components are literally obtainable.index(after:)and the subscript settle for logical indexes (0 byrely) and translate them to the best 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 possibly can see how a lot duty you tackle when you undertake Assortment.
In Abstract
Iteration appears 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 possibly can construct knowledge buildings that really feel pure in Swift, purpose about efficiency, and bridge legacy callbacks into clear async code.
If you wish to maintain 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 practical pipelines. Both means, the subsequent time you attain for for merchandise in checklist, you’ll know precisely what’s occurring beneath the hood and the way to decide on the best strategy for the job.
