List
is the equivalent of UITableView
or NSTableView
.
A simple list of items
struct ListDemo: View {
var items = ["China", "United States"]
var body: some View {
List(items, id: \.self) { item in
Text(item)
}
.listStyle(GroupedListStyle())
}
}
List requires an item’s keypath, to use it as an identifier. For this array of String, simply use itself – \.self
. Custom models should implement the protocol Identifiable
.
The example above uses GroupedListStyle()
.
Selecting a row
There is no UITableViewDelegate
to callback when a row is selected.
The following is a workaround.
List(items, id: \.self) { item in
HStack {
Text(item)
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
// Handle the item
}
The HStack
is used as a row, with a spacer so that the whole width is filled.
The contentShape(Rectangle())
makes it possible to tap on the Spacer()
!
contentShape
defines the content shape for hit testing.
Dismiss the view
This is not about a list, but after selecting a row, you can dismiss the modal view using presentationMode
.
// Declare the environment
@Environment(\.presentationMode) var presentationMode
// Call dismiss
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
Change row background
The row background can be set with listRowBackground()
. But our code above will somehow not work. eg. HStack(...).listRowBackground(Color.blue)
will do nothing.
Strangely, but using ForEach
will work.
List {
ForEach(items, id: \.self) { item in
Text(item)
.listRowBackground(Color.blue)
}
}
And that brings us to the next section, which tells us ForEach
should be preferred over convenience List.init(_: id:)
.
Section, and the use of ForEach
You need multiple ForEach
when you have multiple sections in the table.
List() {
Section(header: Text("Section 1"), footer: Color.blue) {
ForEach(items, id: \.self) { item in
Text(item)
}
}
// Another section
Section() { }
// In fact you can mix any kind of view in a List
Image(systemName: "bolt")
}
Edit, delete, and move
You handle with onDelete()
and onMove()
in the ForEach
(NOT List
).
struct EditListDemo: View {
@State var items = ["iPhone", "iPad", "Apple Watch", "Apple TV"]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete { set in
items.remove(atOffsets: set)
}
.onMove { set, i in
items.move(fromOffsets: set, toOffset: i)
}
}
.navigationBarItems(trailing: EditButton())
}
}
Drag and drop
Drag and drop is via the onInsert()
method.
// Must import this for the UTI types
import MobileCoreServices
.onInsert(of: [String(kUTTypeText)]) { i, itemProviders in
for provider in itemProviders {
if provider.canLoadObject(ofClass: String.self) {
_ = provider.loadObject(ofClass: String.self) { s, error in
self.items.insert(s!, at: i)
}
}
}
}
The above handles 1 type – a text. It can be multiple types.
The NSItemProvider
s will be able to resolve the type, and you insert into your items.
Selection
To select multiple rows, provide the Binding to List.
@State var items = ["black", "lives", "matter"]
@State var selection = Set<String>()
List(items, id: \.self, selection: $selection) {
Text($0)
}
.navigationBarItems(trailing: EditButton())
NOTE: There’s a bug where selection will not work if the list is in a Form.
Remove minimum height of 44 for header or row
When using List
, there is a default minimum height for section header or the rows. For example, if your custom header height is only 20, there will be extra spaces as it is in a container with minimum height of 44.
To remove this restriction, you can do this to your list:
list
.listStyle(.plain)
.environment(\.defaultMinListHeaderHeight, 0)
.environment(\.defaultMinListRowHeight, 0)
Remove row insets and more
That’s not all. If you want to customize your list fully, you have to remove the default row separator, set each row insets to zero and more. Somehow, List
has too many defaults.
There are 3 views (the list, rows and sections) that you can be modifying.
Let’s start with the List
. Other than the modifiers in the section before this, you can also:
list
.listRowSpacing(0)
.listSectionSpacing(0)
Secondly, you have to modify each row view (including headers and footers).
row
.listRowSeparator(.hidden)
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
.listRowBackground(funColor)
Lastly, if you use section,
Section {
...
}
.listSectionSeparator(.hidden)
No promise we can remove all those things that comes with List/UICollectionView. Sometimes, you might need to resort to introspect.