Lists, forms and data-driven views

Scroll large collections efficiently, give rows stable identity, build editable forms, add search and swipe actions, and plot the same data with Swift Charts.

List, ForEach and identity

struct Task: Identifiable, Hashable {
    let id: UUID
    var title: String
    var done: Bool
}

struct TaskList: View {
    @State private var tasks: [Task] = []
    @State private var query = ""

    var filtered: [Task] {
        query.isEmpty ? tasks : tasks.filter { $0.title.localizedCaseInsensitiveContains(query) }
    }

    var body: some View {
        List {
            ForEach(filtered) { task in
                TaskRow(task: task)
                    .swipeActions(edge: .trailing) {
                        Button(role: .destructive) { delete(task) } label: {
                            Label("Delete", systemImage: "trash")
                        }
                    }
            }
            .onMove { from, to in tasks.move(fromOffsets: from, toOffset: to) }
        }
        .searchable(text: $query, prompt: "Filter tasks")
        .listStyle(.plain)
        .refreshable { await reload() }
    }
}
  • Conform to Identifiable with a stable id. Never use the array index: rows animate and scroll to the wrong place when the data changes.
  • List recycles rows lazily; a plain VStack inside a ScrollView builds every row at once.
  • onMove requires an editable list — inside a NavigationStack add .toolbar { EditButton() }.
  • .refreshable expects an async closure; return only after new data has been applied.

Forms and validation

struct SignUpView: View {
    @State private var email = ""
    @State private var age = 18
    @State private var plan = Plan.free
    @State private var accepted = false

    private var emailIsValid: Bool {
        email.contains("@") && email.contains(".")
    }

    var body: some View {
        Form {
            Section("Account") {
                TextField("Email", text: $email)
                    .textContentType(.emailAddress)
                    .keyboardType(.emailAddress)
                    .textInputAutocapitalization(.never)
                    .autocorrectionDisabled()
                Stepper("Age: \(age)", value: $age, in: 13...120)
            }
            Section("Plan") {
                Picker("Plan", selection: $plan) {
                    ForEach(Plan.allCases) { Text($0.title).tag($0) }
                }
                Toggle("Accept terms", isOn: $accepted)
            }
            Section {
                Button("Create account") { submit() }
                    .disabled(!emailIsValid || !accepted)
            }
        }
        .scrollDismissesKeyboard(.interactively)
    }
}
ControlUse forNote
TextFieldFree textSet keyboard and content type or autofill misfires
SecureFieldPasswordsNever log its bound value
PickerSmall closed setsStyle as .segmented for two to four options
DatePickerDates and timesRespect the user's locale by leaving the format alone

A chart from the same data

import Charts

struct ProgressChart: View {
    let points: [(day: String, value: Int)]

    var body: some View {
        Chart(points, id: \.day) { point in
            BarMark(
                x: .value("Day", point.day),
                y: .value("Tasks", point.value)
            )
            .foregroundStyle(by: .value("Series", "Completed"))
        }
        .chartYAxis { AxisMarks(position: .leading) }
        .frame(height: 220)
    }
}
⚠️
Swift Charts is only as good as the data feeding it. Aggregate before you plot — a line mark with a point per raw event over a year will render tens of thousands of shapes and stall the frame.

FAQ

Why do my rows jump when the list updates?
The identity changed. If you build models with UUID() on every refresh, every row is a new item to SwiftUI. Persist the id from the source data instead.
Should I use <code>List</code> or <code>ScrollView</code> with <code>LazyVStack</code>?
Use List for anything that looks like a table: you get swipes, selection, editing and correct separators for free. Reach for LazyVStack only when you need a custom visual that List cannot express.

Layout, stacks and adaptive UI Networking with URLSession and async/await

Last refreshed 2026-09-18.