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

Building editable lists with SwiftUI

Published on 14 Oct 2021
Discover page available: SwiftUI

To say that SwiftUI’s List is the equivalent of UIKit’s UITableView is both true and false at the same time. It’s definitely true that List offers a built-in way to construct list-based UIs that are rendered using the same overall appearance as when using UITableView — however, when it comes to mutations, we instead have to turn to SwiftUI’s core engine for constructing and managing views based on collections of data — the ForEach type.

Moving and deleting

In general, list editing typically involves two separate kinds of edits — item-specific and list-wide ones. To start with the first variant, here’s an example using the list binding syntax that was introduced in Swift 5.5 — in which we’re rendering a TodoList view that enables the user to directly edit the text of each item using a TextField:

struct TodoItem: Identifiable {
    let id: UUID
    var title: String
}

struct TodoList: View {
    @Binding var items: [TodoItem]

    var body: some View {
        NavigationView {
            VStack {
                List {
    ForEach($items) { $item in
        TextField("Title", text: $item.title)
    }
}
                TodoItemAddButton { newItem in
                    items.append(newItem)
                }
            }
            .navigationTitle("Todo list")
        }
    }
}

In this example, our array of TodoItem models is stored outside of our view and is then passed in using a Binding. To learn more about that pattern, check out this guide to SwiftUI’s state management system.

Now let’s say that we wanted to add support for list-wide mutations as well — specifically moves and deletions. The good news is that SwiftUI ships with built-in modifiers for both of those tasks, but it turns out that they’re not available on the List type itself, but rather only on ForEach.

That’s because, in SwiftUI, List can sort of be seen more as a styling component, rather than as a view responsible for managing a collection of subviews. So whenever we wish to mutate such a collection, we need to work directly with ForEach — like this:

struct TodoList: View {
    @Binding var items: [TodoItem]

    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach($items) { $item in
                        TextField("Title", text: $item.title)
                    }
                    .onMove { indexSet, offset in
    items.move(fromOffsets: indexSet, toOffset: offset)
}
.onDelete { indexSet in
    items.remove(atOffsets: indexSet)
}
                }
                ...
            }
            .navigationTitle("Todo list")
        }
    }
}

However, even though our list now technically supports both moves and deletions, there’s currently no way for the user to enter “edit mode” to start moving items. To address that, let’s use the toolbar modifier to insert an instance of SwiftUI’s built-in EditButton into our app’s navigation bar:

struct TodoList: View {
    @Binding var items: [TodoItem]

    var body: some View {
        NavigationView {
            VStack {
                ...
            }
            .navigationTitle("Todo list")
            .toolbar { EditButton() }
        }
    }
}

Note that if you’re working on an iOS app that needs to support iOS 13, you’ll need to use the now deprecated navigationBarItems modifier instead, since the toolbar API was introduced in iOS 14 (and the rest of Apple’s 2020 operating systems).

With that change in place, we now have a fully editable list that supports both inline item edits, as well as list-wide moves and deletions. Really nice!

A reusable abstraction

Depending on what kind of app that we’re working on, we might need to build multiple lists that each should support the above set of editing features — and while it’s certainly possible to accomplish that by simply copying and pasting the modifiers and EditButton code that we just added to our TodoList, it would arguably be much nicer to instead have some form of editable list that we could easily reuse across our code base.

So, let’s build one! Thankfully, because SwiftUI was designed with such a heavy emphasis on composition, implementing a completely reusable EditableList type simply involves moving our previous editing code into that new view’s body, and then adding an initializer that’ll let us inject the data that we wish to render, as well as a closure for constructing the view for each item within our list:

struct EditableList<Element: Identifiable, Content: View>: View {
    @Binding var data: [Element]
    var content: (Binding<Element>) -> Content

    init(_ data: Binding<[Element]>,
         content: @escaping (Binding<Element>) -> Content) {
        self._data = data
        self.content = content
    }

    var body: some View {
        List {
            ForEach($data, content: content)
                .onMove { indexSet, offset in
                    data.move(fromOffsets: indexSet, toOffset: offset)
                }
                .onDelete { indexSet in
                    data.remove(atOffsets: indexSet)
                }
        }
        .toolbar { EditButton() }
    }
}

Note that we don’t strictly need to implement a custom initializer for the above type (unless we want to vend it as public, outside of the module it was defined in), but the benefit of doing so is that our EditableList API now works the same way as SwiftUI’s built-in List, which will make switching between the two much easier.

With the above new type in place, all that we now have to do when we wish to render an editable list is to create an EditableList instance with the array that we want to enable our users to edit — like this:

struct TodoList: View {
    @Binding var items: [TodoItem]

    var body: some View {
        NavigationView {
            VStack {
                EditableList($items) { $item in
    TextField("Title", text: $item.title)
}
                TodoItemAddButton { newItem in
                    items.append(newItem)
                }
            }
            .navigationTitle("Todo list")
        }
    }
}

Really neat! Another benefit of encapsulating our list editing code within a stand-alone type is that we’ll now be able to keep adding editing features in a single location, and all of our editable lists will then get those features for free. For example, we might want to add support for drag and drop, sorting, and so on.

Alright, it’s time for the bonus round! While the above EditableList implementation works perfectly fine as long as our list data always comes in the form of an Array, it would arguably be nice to also make it support any Collection type that List and ForEach are capable of working with (including custom ones).

To make that happen, we’re going to have to change our generic Element type to instead refer to any Data collection that conforms to the same set of standard library protocols that SwiftUI requires to make our list editable. This implementation will have to require iOS 15, though, since SwiftUI’s Binding type gained support for the standard library’s RandomAccessCollection protocol in that OS version:

@available(iOS 15, *)
struct EditableList<
    Data: RandomAccessCollection & MutableCollection & RangeReplaceableCollection,
    Content: View
>: View where Data.Element: Identifiable {
    @Binding var data: Data
    var content: (Binding<Data.Element>) -> Content

    init(_ data: Binding<Data>,
         content: @escaping (Binding<Data.Element>) -> Content) {
        self._data = data
        self.content = content
    }

    var body: some View {
        List {
            ForEach($data, content: content)
                .onMove { indexSet, offset in
                    data.move(fromOffsets: indexSet, toOffset: offset)
                }
                .onDelete { indexSet in
                    data.remove(atOffsets: indexSet)
                }
        }
        .toolbar { EditButton() }
    }
}

We now have a completely generic EditableList implementation that can be used with any compatible collection — with the caveat that it’s only compatible with iOS 15 and later. But, if we need to support earlier iOS versions as well, we could likely just use our previous Array-based version, which is fully backward compatible.

Conclusion

List is arguably one of the most polarizing SwiftUI views. On one hand, it provides a lot of built-in functionality that enables us to relatively easily build lists that look and behave exactly like the ones found throughout Apple’s own apps, as well as iOS itself.

However, although List has become much more flexible since it was introduced in 2019, building completely custom-looking lists using it is still quite difficult. So, for those use cases we’ll likely have to fall back to either UIKit or AppKit, depending on what platform that we’re targeting. Still, even though it might be limited when it comes to its visuals, List is an incredibly useful and powerful part of SwiftUI.

I hope that this article has given you a few insights into exactly what kind of editing capabilities that SwiftUI’s List and ForEach types support, and if you have any questions, comments, or feedback, then feel free to reach out via email.

Thanks for reading!