read

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 NSItemProviders 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.


Image

@samwize

¯\_(ツ)_/¯

Back to Home