One of the most common things you’ll find yourself doing when building UI is dealing with arrays of data.
When all you want to do is read from an array this is fairly straightforward. Things get more complicated when you want to know an item’s offset, index, or to gain mutable access via bindings.
Here I’ll explore a way we can make this effortless going forward.
Our goal:
- An effortless way to access the index and/or offset alongside our elements
- this means we can also access bindings for our elements (using the index)
- Lazy access (perform well regardless of array size)
- Compatible with
ForEach
- including automatic identifiable support if your element is identifiable.
Why not just use
enumerated()
?
I’ll discuss the difference between an item’s index and offset a bit later in this post, but for now lets say that:a) if you’re using the ‘offset’ to access an array it could crash your app
b) you’ll find it won’t actually work with SwiftUI’sForEach
, and won’t respect the fact that your data is identifiable
Implementation:
First let’s make a wrapper type that holds an element as well as its index and offset (these are not always the same!)
struct Indexed<Element, Index>
{
var index: Index
var offset: Int
var element: Element
}
Code language: Swift (swift)
We’ll also add conditional conformance to Identifiable
if our array element is identifiable:
extension Indexed: Identifiable where Element: Identifiable
{
var id: Element.ID { element.id }
}
Code language: Swift (swift)
Now let’s extend RandomAccessCollection
with a method to return an indexed RandomAccessCollection
using our Indexed
type:
extension RandomAccessCollection
{
func indexed() -> AnyRandomAccessCollection<Indexed<Element, Index>>
{
AnyRandomAccessCollection(
zip(zip(indices, 0...).lazy, self).lazy
.map { Indexed(index: $0.0.0, offset: $0.0.1, element: $0.1) }
)
}
}
Code language: Swift (swift)
This might look complicated, but all it is doing is providing lazy access to our elements wrapped in our Indexed
type. This means that we aren’t iterating over our whole array, but rather allowing access to each element on-demand.
As a final touch, lets add @dynamicMemberLookup
support to our Indexed
type, so we can easily access members of our wrapped element:
@dynamicMemberLookup
struct Indexed<Element, Index>
{
var index: Index
var offset: Int
var element: Element
//Access to constant members
subscript<T>(dynamicMember keyPath: KeyPath<Element, T>) -> T
{
element[keyPath: keyPath]
}
//Access to mutable members
subscript<T>(dynamicMember keyPath: WritableKeyPath<Element, T>) -> T
{
get { element[keyPath: keyPath] }
set { element[keyPath: keyPath] = newValue }
}
}
Code language: Swift (swift)
Using .indexed()
Time for an example! For a very artificial scenario: let’s say we want to present a numbered list of animals…
First we’ll define an Animal
type:
struct Animal: Identifiable {
var id = UUID()
var name: String
var haveSeen: Bool = false
}
Code language: Swift (swift)
Now to make our list using .indexed()
on our array of animals:
struct AnimalsListView : View {
// Here's some placeholder data for our example
// Declared @State because we'll mutate it in the next example
@State var animals: [Animal] = [
Animal(label: "Tiger"),
Animal(label: "Lion"),
Animal(label: "Emu"),
Animal(label: "Giraffe"),
Animal(label: "Elephant"),
Animal(label: "Panda"),
]
var body: some View {
List {
ForEach(items.indexed()) { indexedAnimal in
Text("\(indexedAnimal.offset + 1) - \(indexedAnimal.label)")
}
}
}
}
Code language: Swift (swift)

And that’s all we need to effortlessly add numbering to our list
//If you're using a playground:
import PlaygroundSupport
PlaygroundPage.current.setLiveView(AnimalsListView())
Code language: Swift (swift)
Using Bindings inside our ForEach
We’ve used offset
for our numbering above, as it will always start from zero and count up (great for numbering our list). However, if we’re accessing a collection, we want to use the index
.
Indices often start at zero, but not always….
For example if you use an
ArraySlice
such asanimalsSubgroup = animals[3...5]
, the first index will actually be 3! If you tried to then accessanimalsSubgroup[0]
your app would crash.
Now let’s update our ForEach loop to add a toggle denoting whether we haveSeen
each animal (just a mutable boolean value declared in our model):
ForEach(animals.indexed()) { indexedAnimal in
Toggle(isOn: self.$animals[indexedAnimal.index].haveSeen) {
Text("\(indexedAnimal.offset + 1) - \(indexedAnimal.name)")
}
}
Code language: Swift (swift)

And just like that we can toggle the haveSeen
value of our animals.
Sidenote: This is poor UI design for the sake of a simple example… it is not at all clear to the user what this toggle represents!
Conclusion
This is a fairly straightforward implementation but should save a lot of boilerplate code going forward! Feel free to comment if you have any suggestions for improvement.
Sidenote: There is a bug in how SwiftUI handles deletions from ForEach currently, see this post if you encounter crashes on deletion of items.