Lists
SwiftUIs List is the modern table view: declarative, diffable, search-aware, swipe actions, and pull-to-refresh out of the box. Pair with .listStyle and ForEach for the look you want, and reach for LazyVStack inside a ScrollView only when List style does not fit.
List patterns: sections, swipes, search, refresh, drag
EXAMPLE
import SwiftUI
// 1) Model
struct Order: Identifiable, Hashable {
let id: UUID
let customer: String
let totalCents: Int
let status: String
}
// 2) Plain list with section + swipe actions
struct OrdersList: View {
@State private var orders: [Order] = (1...30).map { i in
Order(id: UUID(), customer: "Customer \(i)", totalCents: i * 1000,
status: i.isMultiple(of: 3) ? "paid" : "new")
}
@State private var query = ""
var filtered: [Order] {
guard !query.isEmpty else { return orders }
return orders.filter { $0.customer.localizedCaseInsensitiveContains(query) }
}
var body: some View {
NavigationStack {
List {
Section("Open") {
ForEach(filtered.filter { $0.status == "new" }) { row($0) }
}
Section("Paid") {
ForEach(filtered.filter { $0.status == "paid" }) { row($0) }
}
}
.listStyle(.insetGrouped)
.navigationTitle("Orders")
.searchable(text: $query)
.refreshable { await refresh() }
}
}
@ViewBuilder
func row(_ o: Order) -> some View {
NavigationLink(value: o) {
HStack {
VStack(alignment: .leading) {
Text(o.customer).font(.body.weight(.medium))
Text(o.status).font(.caption).foregroundStyle(.secondary)
}
Spacer()
Text("\(Double(o.totalCents) / 100, specifier: "\\(\"$%.2f\")")")
.monospacedDigit()
}
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
orders.removeAll { $0.id == o.id }
} label: { Label("Delete", systemImage: "trash") }
}
.swipeActions(edge: .leading) {
Button {
if let i = orders.firstIndex(where: { $0.id == o.id }) {
var copy = orders[i]; copy = Order(id: copy.id, customer: copy.customer,
totalCents: copy.totalCents, status: "paid")
orders[i] = copy
}
} label: { Label("Pay", systemImage: "checkmark") }
.tint(.green)
}
}
func refresh() async {
try? await Task.sleep(nanoseconds: 800_000_000)
// refresh from API here
}
}
// 3) Reorderable list
struct ReorderableList: View {
@State private var items = ["Apple", "Banana", "Cherry", "Date"]
@State private var editMode: EditMode = .inactive
var body: some View {
NavigationStack {
List {
ForEach(items, id: \.self) { Text($0) }
.onMove { from, to in items.move(fromOffsets: from, toOffset: to) }
.onDelete { offsets in items.remove(atOffsets: offsets) }
}
.toolbar { EditButton() }
.environment(\.editMode, $editMode)
}
}
}
// 4) LazyVStack — when List does not fit (custom backgrounds, infinite scroll, etc.)
struct CustomList: View {
let items: [Order]
var body: some View {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(items) { o in
HStack {
Text(o.customer)
Spacer()
Text(o.status)
}
.padding()
Divider()
}
}
}
}
}
// 5) Pull-to-refresh works on any List; for LazyVStack use the Scroll APIs directly.
// 6) Common pitfalls
// - Using ForEach without id when the data is not Identifiable -> diffing breaks
// - Heavy row content -> measure with the Instruments View Body recorder
// - Custom row backgrounds inside List -> use .listRowBackground / .listRowSeparator(.hidden)
// - Forgetting EditButton in toolbar when using onDelete/onMove
// 7) Decision matrix
// - Standard rows + sections + swipe actions -> List + Section
// - Reorderable -> List + .onMove + EditButton
// - Custom row appearance / background -> List + .listRowBackground
// - Need full visual control / virtualised columns -> LazyVStack inside ScrollView
// - Tens of thousands of rows -> List (it virtualises) + paginate
Why it matters
`List` does diffing, recycling, swipe actions, search and refresh for free. Reach for LazyVStack only when List style does not fit; trying to rebuild Lists features by hand is the surest way to ship a slow, bug-prone scroll on day one.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
List {
ForEach(items) { i in
Text(i.title)
}
.onDelete { idx in items.remove(atOffsets: idx) }
}
Try it Yourself »
Discussion
Loading…