Using enumerated() with SwiftUI List and ForEach to show item numbers
When displaying items in SwiftUI, we sometimes want to include their position in the sequence, for example, to show a list of instructions or ranked results. A common way to achieve this is by calling enumerated() on the collection. This returns an EnumeratedSequence
, which is a sequence of (offset, element)
pairs, where offset
is a counter starting from zero and element
is the corresponding value from the original collection.
Until recently, enumerated()
wasn’t directly compatible with ForEach
or List
in SwiftUI, because the result did not conform to RandomAccessCollection
. As a workaround, we had to wrap the sequence in an array.
RecipeStepsView: View {
let steps = [
"Chop lettuce, tomatoes, and cucumber.",
"Drizzle with olive oil and lemon juice.",
"Toss gently and serve."
]
var body: some View {
VStack(alignment: .leading) {
ForEach(
Array(steps.enumerated()), id: \.element
) { offset, step in
Text("\(offset + 1). \(step)")
}
}
}
}
As of Swift 6.2 and iOS 26, this extra step is no longer required. Proposal SE-0459 adds Collection
, RandomAccessCollection
, and BidirectionalCollection
conformance to EnumeratedSequence
when the base collection supports those protocols.
ForEach(
steps.enumerated(), id: \.element
) { offset, step in
Text("\(offset + 1). \(step)")
}
It’s important to note that the offset
in EnumeratedSequence
is a counter and should not be used as an index to access elements from the original collection. While it may appear to work when the base collection is zero-based and uses integer indices, this assumption breaks down in other scenarios, for example, when iterating over a SubSequence
or any collection that does not start at zero. For more details and recommended approaches, see my other post: Iterate over items and indices in Swift collections.
Another important point to consider is that neither offset nor an index should be used as the id
in ForEach
unless the collection is immutable and the ordering is guaranteed to remain stable. In dynamic collections where elements can be added, removed, or reordered, such as in user-editable lists, using a counter or index as the identifier can result in incorrect view updates and animations. In these cases, a stable and unique identifier should be used instead, either by conforming the element type to Identifiable
or by supplying a unique key path.
struct Task: Identifiable {
var id = UUID()
var title: String
}
struct TaskListView: View {
@State private var tasks = [
Task(title: "Buy vegetables"),
Task(title: "Prep dressing"),
Task(title: "Set the table")
]
var body: some View {
List {
ForEach(
tasks.enumerated(),
id: \.element.id
) { offset, task in
Text("\(offset + 1). \(task.title)")
}
.onDelete { indices in
tasks.remove(atOffsets: indices)
}
}
}
}
This pattern keeps the display numbering from enumerated()
while maintaining stable identity through the task’s id
. By decoupling numbering from identity, it ensures correct behavior as the data changes.
If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.
For more resources on Swift and SwiftUI, check out my other books and book bundles.