SwiftUI.cc
Snippets
Lists & GridsListBeginner

SwiftUI List: ForEach, Selection, Move, and Delete

List renders platform-native rows from data. ForEach adds onDelete and onMove editing, and a selection binding turns rows tappable with single or multi-select.

5 min readUpdated 2026-06
Editable packing list with multi-select
import SwiftUI

struct PackingList: View {
    @State private var items = ["Passport", "Charger", "Camera", "Jacket"]
    @State private var selection = Set<String>()

    var body: some View {
        NavigationStack {
            List(selection: $selection) {
                ForEach(items, id: \.self) { item in
                    Text(item)
                }
                .onDelete { items.remove(atOffsets: $0) }
                .onMove { items.move(fromOffsets: $0, toOffset: $1) }
            }
            .navigationTitle("Packing")
            .toolbar { EditButton() }
        }
    }
}

List selection and editing preview

Two ways to feed a List

List(items) { item in … } is the compact form. The moment you need deletion, reordering, or mixed static-and-dynamic content, switch to the List { ForEach(items) { … } } form — the editing modifiers live on ForEach:

ForEach(items) { item in Row(item) }
    .onDelete { offsets in items.remove(atOffsets: offsets) }
    .onMove   { from, to in items.move(fromOffsets: from, toOffset: to) }

Selection

Bind selection: to an optional for single-select or a Set for multi-select. On iOS, multi-select activates in edit mode (or with two-finger swipes); on macOS and iPadOS with pointer, selection works directly. selectionDisabled(_:) opts individual rows out — separators between selectable and informational rows.

Identity is everything

Move and delete operate on offsets, but row animation and state preservation key off identity. Use Identifiable models with stable ids; id: \.self on strings is fine until duplicates appear, at which point edits target the wrong row.

Static and dynamic together

Because rows are just views, you can mix a static header row, a ForEach, and a static footer row in one List — something table views made tedious.

Common mistakes

  • Putting onDelete on List instead of ForEach and wondering why nothing happens.
  • Index-based ids that shift after deletion, breaking animations.
  • Recomputing filtered arrays inside body without memoization for huge lists.

Related reference