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

Bindable SwiftUI list elements

Remastered on 17 Jun 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.

RevenueCat

RevenueCat: Easily build and manage iOS and Android in-app purchases. With just a few lines of code RevenueCat provides IAP infrastructure, customer analytics, data integrations, and gives you time back from dealing with edge cases and updates across all platforms.

Not all values are bindings

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 closure are all immutable, non-bindable values.

Xcode 13’s new element binding syntax

Now, if we’re working on a project that’s built using Xcode 13 (which is currently in beta at the time of writing), then we’re in luck, because there’s a new syntax that lets us automatically convert a given collection’s elements into bindable values.

All that we have to do is to reference our property, and our ForEach closure argument, using the same $ syntax that we typically use when generating bindings, and the system will take care of the rest:

struct NoteListView: View {
    @ObservedObject var list: NoteList

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

Note that we can reference our closure’s $note input either with or without its dollar prefix, depending on whether we want to access the underlying value directly, or the binding that encapsulates it.

Something that’s really nice about the above new syntax is that it’s actually fully backward compatible with all previous operating systems on which SwiftUI is supported — so on iOS, that means as far back as iOS 13. The only requirement is that we have to build our app using the compiler and SDK that’s included in Xcode 13.

Solving the problem when using earlier Xcode versions

However, if we’re working on a project that’s not yet using Xcode 13 and the SDKs that are bundled with it, then we’ll have to explore a few other, more custom solutions.

One option would be 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 the above code 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.

Identifiable indices

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.

Custom bindings

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
        )
    )
}
        }
    }
}

Support Swift by Sundell by checking out this sponsor:

RevenueCat

RevenueCat: Easily build and manage iOS and Android in-app purchases. With just a few lines of code RevenueCat provides IAP infrastructure, customer analytics, data integrations, and gives you time back from dealing with edge cases and updates across all platforms.

Conclusion

It’s really great that Apple addressed the issue of creating bindings to a collection’s elements in the 2021 release of SwiftUI and the Swift compiler, but if we’re not yet ready to fully migrate to those toolchains, then we can also create a more custom workaround using some of Swift’s built-in protocols, such as RandomAccessCollection and Identifiable.

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