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
Identifiablewith a stable id. Never use the array index: rows animate and scroll to the wrong place when the data changes. Listrecycles rows lazily; a plainVStackinside aScrollViewbuilds every row at once.onMoverequires an editable list — inside aNavigationStackadd.toolbar { EditButton() }..refreshableexpects 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)
}
}| Control | Use for | Note |
|---|---|---|
TextField | Free text | Set keyboard and content type or autofill misfires |
SecureField | Passwords | Never log its bound value |
Picker | Small closed sets | Style as .segmented for two to four options |
DatePicker | Dates and times | Respect 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.Related
Layout, stacks and adaptive UI Networking with URLSession and async/await
Last refreshed 2026-09-18.