List in VStack SwiftUI

Building editable lists with SwiftUI

  • swiftui
  • Swift 5.5
Discover page available: SwiftUI

To say that SwiftUIs List is the equivalent of UIKits UITableView is both true and false at the same time. Its 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 SwiftUIs core engine for constructing and managing views based on collections of data the ForEach type.

Bitrise: Kick off 2022 by easily setting up fast, rock-solid continuous integration for your project with Bitrise. In just a few minutes, you can set up builds, tests, and automatic App Store and beta deployments for your project, all running in the cloud on every pull request and commit. Try it for free today.

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, heres an example using the list binding syntax that was introduced in Swift 5.5 in which were 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 SwiftUIs state management system.

Now lets 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 theyre not available on the List type itself, but rather only on ForEach.

Thats 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, theres currently no way for the user to enter edit mode to start moving items. To address that, lets use the toolbar modifier to insert an instance of SwiftUIs built-in EditButton into our apps navigation bar:

struct TodoList: View { @Binding var items: [TodoItem] var body: some View { NavigationView { VStack { ... } .navigationTitle["Todo list"] .toolbar { EditButton[] } } } }

Note that if youre working on an iOS app that needs to support iOS 13, youll need to use the now deprecated navigationBarItems modifier instead, since the toolbar API was introduced in iOS 14 [and the rest of Apples 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 were working on, we might need to build multiple lists that each should support the above set of editing features and while its 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, lets 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 views body, and then adding an initializer thatll 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: View { @Binding var data: [Element] var content: [Binding] -> Content init[_ data: Binding, content: @escaping [Binding] -> 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 dont 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 SwiftUIs 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 well 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, its 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, were 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 SwiftUIs Binding type gained support for the standard librarys 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] -> Content init[_ data: Binding, content: @escaping [Binding] -> 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 its 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.

Support Swift by Sundell by checking out this sponsor:

Bitrise: Kick off 2022 by easily setting up fast, rock-solid continuous integration for your project with Bitrise. In just a few minutes, you can set up builds, tests, and automatic App Store and beta deployments for your project, all running in the cloud on every pull request and commit. Try it for free today.

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 Apples 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 well likely have to fall back to either UIKit or AppKit, depending on what platform that were 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 SwiftUIs 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!

  • Share this article on Twitter

More on similar topics

Browse all content by tag
  • Podcast episode

    94: A Mac-like Mac app with special guest Benedikt Terhechte

  • Article

    Previewing SwiftUI views in landscape

  • Article

    Rendering textured views with SwiftUI

Video liên quan

Chủ Đề