Articles, podcasts and news about Swift development, by John Sundell.

Bindable SwiftUI list elements

Published on 12 Mar 2021
Discover page available: SwiftUI

SwiftUI’s Binding property wrapper lets us establish a two-way binding between a given piece of state and any view that wishes to modify that state. Typically, creating such a binding simply involves referencing the state property that we wish to bind to using the $ prefix, but when it comes to collections, things are often not quite as straightforward.

For example, let’s say that we’re building a note taking app, and that we’d like to bind each Note model within an array to a series of NoteEditingView instances that are being created within a SwiftUI ForEach loop — like this:

struct Note: Hashable, Identifiable {
    let id: UUID
    var title: String
    var text: String
    ...
}

class NoteList: ObservableObject {
    @Published var notes: [Note]
    ...
}

struct NoteEditView: View {
    @Binding var note: Note

    var body: some View {
        ...
    }
}

struct NoteListView: View {
    @ObservedObject var list: NoteList

    var body: some View {
        List {
            ForEach(list.notes) { note in
                NavigationLink(note.title,
                    destination: NoteEditView(
    note: $note
)
                )
            }
        }
    }
}

Unfortunately, the above code sample doesn’t fully compile, for the same reason that we can’t directly mutate a value within a classic for loop — the note arguments that are being passed into our ForEach are all immutable, non-bindable values.

Instead, let’s try to iterate over the indices of our notes array, which will let us bind to mutable versions of our Note models by subscripting into $list.notes using the index that’s now passed into our ForEach closure:

struct NoteListView: View {
    @ObservedObject var list: NoteList

    var body: some View {
        List {
            ForEach(list.notes.indices) { index in
    NavigationLink(list.notes[index].title,
        destination: NoteEditView(
            note: $list.notes[index]
        )
    )
}
        }
    }
}

While our code now successfully compiles, and might initially even seem to be fully working — as soon as we’ll mutate our array of notes, we’ll get the following warning printed within the Xcode console:

ForEach(_:content:) should only be used for *constant* data. Instead conform
data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!

Alright, so let’s do what SwiftUI tells us, by passing an explicit id key path when creating our ForEach instance — like this:

struct NoteListView: View {
    @ObservedObject var list: NoteList

    var body: some View {
        List {
            ForEach(list.notes.indices, id: \.self) { index in
                NavigationLink(list.notes[index].title,
                    destination: NoteEditView(
                        note: $list.notes[index]
                    )
                )
            }
        }
    }
}

At this point, we might have actually solved the problem. There are no more warnings being emitted, and things might continue to work perfectly fine even as we mutate our Note array. However, “might” is really the keyword here, as what we’ve essentially done is to make the index of each note its “reuse identifier”. What that means is that we might run into certain odd behaviors (or crashes, even) if our array ever changes rapidly, as SwiftUI will now consider each note’s index a stable identifier for that particular model and its associated NavigationLink.

So to truly fix the problem, we’re either going to have to refactor our NoteList class to also offer a way to access each Note by its proper UUID-based id (which would let us pass an array of those ids to ForEach, rather than using Int-based array indices), or we’re going to have to dive a bit deeper into Swift’s collection APIs in order to make our array indices truly unique.

In this case, let’s go for the second strategy, by introducing a custom collection that’ll combine the indices of another collection with the identifiers of the elements that it contains. To get started, let’s define a new type called IdentifiableIndices, which wraps a Base collection and also declares an Index and an Element type:

struct IdentifiableIndices<Base: RandomAccessCollection>
    where Base.Element: Identifiable {

    typealias Index = Base.Index

    struct Element: Identifiable {
        let id: Base.Element.ID
        let rawValue: Index
    }

    fileprivate var base: Base
}

Next, let’s make our new collection conform to the standard library’s RandomAccessCollection protocol, which mostly involves forwarding the required properties and methods to our underlying base collection — except for the implementation of subscript, which returns an instance of the Element type that we defined above:

extension IdentifiableIndices: RandomAccessCollection {
    var startIndex: Index { base.startIndex }
    var endIndex: Index { base.endIndex }

    subscript(position: Index) -> Element {
    Element(id: base[position].id, rawValue: position)
}

    func index(before index: Index) -> Index {
        base.index(before: index)
    }

    func index(after index: Index) -> Index {
        base.index(after: index)
    }
}

That’s it! Our new collection is now ready for action. However, to make it a bit more convenient to use, let’s also introduce two small extensions that’ll heavily improve its overall ergonomics. First, let’s make it easy to create an IdentifiableIndices instance by adding the following computed property to all compatible base collections (that is, ones that support random access, and also contains Identifiable elements):

extension RandomAccessCollection where Element: Identifiable {
    var identifiableIndices: IdentifiableIndices<Self> {
        IdentifiableIndices(base: self)
    }
}

The reason we can confidently make the above a computed property, rather than a method, is because IdentifiableIndices computes its elements lazily. That is, it doesn’t iterate over its base collection when first created, but rather acts more like a lens into that collection’s indices and identifiers. So creating it is an O(1) operation.

Finally, let’s also extend SwiftUI’s ForEach type with a convenience API that’ll let us iterate over an IdentifiableIndices collection without also having to manually access the rawValue of each index:

extension ForEach where ID == Data.Element.ID,
                        Data.Element: Identifiable,
                        Content: View {
    init<T>(
        _ indices: Data,
        @ViewBuilder content: @escaping (Data.Index) -> Content
    ) where Data == IdentifiableIndices<T> {
        self.init(indices) { index in
            content(index.rawValue)
        }
    }
}

With the above pieces in place, we can now go back to our NoteListView and make its usage of ForEach much more stable and reliable by making it iterate over our Note array’s identifiableIndices — like this:

struct NoteListView: View {
    @ObservedObject var list: NoteList

    var body: some View {
        List {
            ForEach(list.notes.identifiableIndices) { index in
                NavigationLink(list.notes[index].title,
                    destination: NoteEditView(
                        note: $list.notes[index]
                    )
                )
            }
        }
    }
}

However, while the above solution should prove to work really well in many different kinds of situations, it’s still possible to encounter crashes and other bugs if the last element of our collection is ever removed. It seems like SwiftUI applies some form of caching to the collection bindings that it creates, which can cause an outdated index to be used when subscripting into our underlying Note array — and if that happens when the last element was removed, then our app will crash with an out-of-bounds error. Not great.

While this certainly seems to be a bug within SwiftUI itself, it’s still something that we can work around locally for now. Rather than using SwiftUI’s built-in API for retrieving nested bindings for each collection element, let’s instead create custom Binding instances, which (at least in my experience) will completely solve the problem.

To make that happen, let’s modify our previous ForEach extension to instead accept a Binding reference to the collection that we wish to iterate over (which, in turn, requires that collection to conform to MutableCollection), and to then use that to create custom Binding instances for getting and setting each element. Finally, we’ll pass each such custom binding to our content closure, along with the index of the current element — like this:

extension ForEach where ID == Data.Element.ID,
                        Data.Element: Identifiable,
                        Content: View {
    init<T>(
        _ data: Binding<T>,
        @ViewBuilder content: @escaping (T.Index, Binding<T.Element>) -> Content
    ) where Data == IdentifiableIndices<T>, T: MutableCollection {
        self.init(data.wrappedValue.identifiableIndices) { index in
            content(
                index.rawValue,
                Binding(
    get: { data.wrappedValue[index.rawValue] },
    set: { data.wrappedValue[index.rawValue] = $0 }
)
            )
        }
    }
}

If we now use the above new API to update our NoteListView to instead look like this, then we should be able to modify our NoteList model object however we please without encountering any kind of SwiftUI-related issues within our view:

struct NoteListView: View {
    @ObservedObject var list: NoteList

    var body: some View {
        List {
            ForEach($list.notes) { index, note in
    NavigationLink(note.wrappedValue.title,
        destination: NoteEditView(
            note: note
        )
    )
}
        }
    }
}

While I do hope that future SwiftUI releases will add new convenience APIs for creating bindings to a collection’s elements, the fact that SwiftUI uses standard Swift protocols (such as RandomAccessCollection and Identifiable) to drive its logic means that we can very often augment it to fit our needs, or to temporarily work around bugs and other issues, which is a huge benefit in cases like this.

Thanks for reading, and feel free to reach out via either Twitter or email if you have any questions, comments or feedback.

Support Swift by Sundell by checking out this sponsor:

Building Mobile Apps at Scale

Building Mobile Apps at Scale: Based on learnings from scaling the development of the Uber app over four years — this free, 200-page book will help you overcome 39 of the most commonly faced challenges when building large iOS apps. The book is free to download for a limited time, so grab your copy now.