← Back to archive

Development · SwiftUI

Building and editing SwiftUI lists

Create rows and sections, add deletion and reordering, and customize the list’s appearance.

Read the Korean original on Velog ↗English translation

Development archive cover

A list starts as a vertical collection of data, but a real screen often needs groups, deletion, reordering, and new items. Expanding a small shopping list shows where each capability belongs.

List content

Pass views to a List to display them as rows.

List {
	Text("apple") // First row
	Text("banana") // Second row
	Text("oragne") // Third row
}

Use ForEach with identifiable data to generate rows dynamically.

struct ListStudy: View {
    
    @State var fruits: [String] = [
        "apple", "banana", "orange"
    ]
    
    var body: some View {
        List {
        	// List displaying the fruits array
            ForEach(fruits, id: \.self) { fruit in
                Text(fruit)
            }
        }
    }
}

Sections

A Section groups related rows and can include a header and footer.

struct ListStudy: View {
    
    @State var fruits: [String] = [
        "apple", "banana", "orange"
    ]
    
    var body: some View {
        List {
            Section { // Add a section
                ForEach(fruits, id: \.self) { fruit in
                    Text(fruit)
                }
            } header: { // Add a header
                Text("Fruits")
            }
        }
    }
}

Deleting rows

onDelete receives an IndexSet identifying the rows to remove. IndexSet, from Foundation, represents a set of integer indices.

struct ListStudy: View {
    
    @State var fruits: [String] = [
        "apple", "banana", "orange"
    ]
    
    var body: some View {
        List {
            Section {
                ForEach(fruits, id: \.self) { fruit in
                    Text(fruit)
                }
                // Delete items from the list
                .onDelete { indexSet in
                    fruits.remove(atOffsets: indexSet)
                }
            } header: {
                Text("Fruits")
            }
        }
    }
}

Extract fruits.remove(atOffsets: indexSet) into a delete function, then pass that function to the perform parameter of onDelete.

struct ListStudy: View {
    
    @State var fruits: [String] = [
        "apple", "banana", "orange"
    ]
    
    var body: some View {
        List {
            Section {
                ForEach(fruits, id: \.self) { fruit in
                    Text(fruit)
                }
                // Pass delete to the perform parameter
                .onDelete(perform: delete(indexSet:))
            } header: {
                Text("Fruits")
            }
        }
    }
    
    func delete(indexSet: IndexSet) {
        fruits.remove(atOffsets: indexSet)
    }
}

Passing the function directly avoids writing a separate closure just to forward its argument.

struct ListStudy: View {
    
    @State var fruits: [String] = [
        "apple", "banana", "orange"
    ]
    
    var body: some View {
        List {
            Section {
                ForEach(fruits, id: \.self) { fruit in
                    Text(fruit)
                }
                // Pass the delete function directly
                .onDelete(perform: delete)
            } header: {
                Text("Fruits")
            }
        }
    }
    
    func delete(indexSet: IndexSet) {
        fruits.remove(atOffsets: indexSet)
    }
}

Reordering rows

onMove lets the user change row order. In this example, the list sits inside a NavigationView, and an EditButton() enables the editing controls.

struct ListStudy: View {
    
    @State var fruits: [String] = [
        "apple", "banana", "orange"
    ]
    
    var body: some View {
    	// Add a NavigationView
        NavigationView {
            List {
                Section {
                    ForEach(fruits, id: \.self) { fruit in
                        Text(fruit)
                    }
                    .onDelete(perform: delete)
                    // Reorder list items
                    .onMove { indices, newOffset in
                        fruits.move(fromOffsets: indices, toOffset: newOffset)
                    }
                } header: {
                    Text("Fruits")
                }
            }
            .navigationTitle("Grocery List")
            .navigationBarItems(leading: EditButton()) // Add an EditButton
        }
    }
    
    func delete(indexSet: IndexSet) {
        fruits.remove(atOffsets: indexSet)
    }
}

Extract fruits.move(fromOffsets: indices, toOffset: newOffset) into a move function and pass it to onMove, just as with deletion.

struct ListStudy: View {
    
    @State var fruits: [String] = [
        "apple", "banana", "orange"
    ]
    
    var body: some View {
        NavigationView {
            List {
                Section {
                    ForEach(fruits, id: \.self) { fruit in
                        Text(fruit)
                    }
                    .onDelete(perform: delete)
                    // Pass move directly to the perform parameter
                    .onMove(perform: move)
                } header: {
                    Text("Fruits")
                }
            }
            .navigationTitle("Grocery List")
            .navigationBarItems(leading: EditButton())
        }
    }
    
    func move(indices: IndexSet, newOffset: Int) {
        fruits.move(fromOffsets: indices, toOffset: newOffset)
    }
    func delete(indexSet: IndexSet) {
        fruits.remove(atOffsets: indexSet)
    }
}

Adding rows

A button in the navigation bar can append an item to the list's data.

struct ListStudy: View {
    
    @State var fruits: [String] = [
        "apple", "banana", "orange"
    ]
    
    var body: some View {
        NavigationView {
            List {
                Section {
                    ForEach(fruits, id: \.self) { fruit in
                        Text(fruit)
                    }
                    .onDelete(perform: delete)
                    .onMove(perform: move)
                } header: {
                    Text("Fruits")
                }
            }
            .navigationTitle("Grocery List")
            .navigationBarItems(
                leading: EditButton(),
                // Add a button for new items
                trailing:
                    Button("Add", action: {
                        add()
                    })
            )
        }
    }
	
    // Define the add-item function
    func add() {
        fruits.append("peach")
    }
    func move(indices: IndexSet, newOffset: Int) {
        fruits.move(fromOffsets: indices, toOffset: newOffset)
    }
    func delete(indexSet: IndexSet) {
        fruits.remove(atOffsets: indexSet)
    }
}

The add button can also be extracted into its own view property.

struct ListStudy: View {
    
    @State var fruits: [String] = [
        "apple", "banana", "orange"
    ]
    
    var body: some View {
        NavigationView {
            List {
                Section {
                    ForEach(fruits, id: \.self) { fruit in
                        Text(fruit)
                    }
                    .onDelete(perform: delete)
                    .onMove(perform: move)
                } header: {
                    Text("Fruits")
                }
            }
            .navigationTitle("Grocery List")
            .navigationBarItems(
                leading: EditButton(),
                trailing: addButton // Add-item button view
            )
        }
    }
    
    // Define the add-item button view
    var addButton: some View {
        Button("Add") {
            add()
        }
    }
    
    func add() {
        fruits.append("peach")
    }
    func move(indices: IndexSet, newOffset: Int) {
        fruits.move(fromOffsets: indices, toOffset: newOffset)
    }
    func delete(indexSet: IndexSet) {
        fruits.remove(atOffsets: indexSet)
    }
}

List styles

Apply a listStyle modifier to choose the list's overall presentation.

NavigationView {
	List {
		Section {
			ForEach(fruits, id: \.self) { fruit in
				Text(fruit)
			}
			.onDelete(perform: delete)
			.onMove(perform: move)
		} header: {
			Text("Fruits")
		}
	}
	.listStyle(.grouped) // Change the list style
	.navigationTitle("Grocery List")
	.navigationBarItems(
		leading: EditButton(),
		trailing: addButton
	)
}

Customizing appearance

Modifiers such as accentColor and listRowBackground adjust the appearance of the list and its rows.

NavigationView {
    List {
        Section {
            ForEach(fruits, id: \.self) { fruit in
                Text(fruit)
                	// Style the row
                    .font(.headline)
                    .foregroundColor(.white)
                    .padding()
            }
            .onDelete(perform: delete)
            .onMove(perform: move)
            // Set the row background
            .listRowBackground(Color.blue)
        } header: {
        	// Add a header image
            HStack {
                Image(systemName: "flame.fill")
                Text("Fruits")
            }
        }
    }
    // Change the list's accent color
    .accentColor(.green)
    .navigationTitle("Grocery List")
    .navigationBarItems(
        leading: EditButton(),
        trailing: addButton
    )
}
// Change the navigation accent color
.accentColor(.red)

← Back to archive