6.1 List — 列表视图
List 是 SwiftUI 中最常用的视图之一,用于展示一组数据项。它内置了滚动、分割线、系统样式等功能,并且支持懒加载(只渲染可见的行),适合大量数据展示。
静态列表
静态列表的内容在编译时固定,适合设置页、菜单等固定选项列表。
// 静态列表:直接在 List 内写子视图
List {
Text("第一行")
Text("第二行")
HStack {
Image(systemName: "gear")
Text("设置")
}
}
// listStyle:列表外观样式
List { /* ... */ }
.listStyle(.plain) // 纯净,无背景
// .listStyle(.insetGrouped) // 圆角分组(设置页常用)
// .listStyle(.grouped) // 分组,无圆角
// .listStyle(.sidebar) // iPad 侧边栏样式
动态列表
动态列表根据数据数组自动生成行,使用 ForEach 或直接向 List 传入集合。
let fruits = ["苹果", "香蕉", "橙子", "葡萄"]
// 方式1:List 直接接收集合(需要 id 参数)
List(fruits, id: \.self) { fruit in
Text(fruit)
}
// 方式2:List + ForEach(更灵活,可混合静态和动态内容)
List {
Text("水果列表").font(.headline) // 静态头部
ForEach(fruits, id: \.self) { fruit in
Label(fruit, systemImage: "leaf")
}
Text("共 \(fruits.count) 种").foregroundColor(.secondary)
}
6.2 ForEach 与 Identifiable 协议
ForEach 是根据集合数据生成多个视图的容器。每个元素需要有唯一标识符,这样 SwiftUI 才能在数据变化时高效地更新视图(只更新发生变化的行,而不是全部重建)。
Identifiable 协议
Identifiable 协议要求类型提供一个 id 属性。遵循此协议的类型在 ForEach 和 List 中使用时无需显式指定 id: 参数。
// Identifiable:提供稳定的唯一标识
struct TodoItem: Identifiable {
let id = UUID() // UUID 保证唯一性
var title: String
var isDone: Bool = false
var priority: Int = 1
}
// 遵循 Identifiable 后,ForEach 自动用 id 属性
let todos: [TodoItem] = [/* ... */]
List {
ForEach(todos) { todo in // 无需写 id: \.id
Text(todo.title)
}
}
// 对于没有实现 Identifiable 的类型,手动指定 id
ForEach(["a", "b", "c"], id: \.self) { str in
Text(str)
}
// 数字范围(注意:Range 不是 Identifiable,需要 id: \.self)
ForEach(0..<10) { i in // Range 实现了特殊处理
Text("项目 \(i)")
}
6.3 列表行操作:删除与移动
SwiftUI 的 List 内置了滑动删除和拖拽移动功能,只需在 ForEach 上添加相应修饰符即可。
struct EditableListView: View {
@State private var items = ["苹果", "香蕉", "橙子", "葡萄"]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
// onDelete:向左滑动出现删除按钮
.onDelete { indexSet in
// indexSet 包含要删除的行索引
items.remove(atOffsets: indexSet)
}
// onMove:长按后拖拽移动行
.onMove { source, destination in
// source: 源位置集合,destination: 目标位置
items.move(fromOffsets: source, toOffset: destination)
}
}
// EditButton:系统内置编辑模式切换按钮
// 进入编辑模式后会显示删除按钮和移动手柄
.toolbar { EditButton() }
}
}
// 自定义滑动操作(iOS 15+)
List {
ForEach(items, id: \.self) { item in
Text(item)
// swipeActions:完全自定义滑动操作
.swipeActions(edge: .trailing) { // 从右向左滑
Button(role: .destructive) {
// 删除操作
} label: {
Label("删除", systemImage: "trash")
}
Button {
// 归档
} label: {
Label("归档", systemImage: "archivebox")
}
.tint(.orange)
}
.swipeActions(edge: .leading) { // 从左向右滑
Button {
// 标记为已读
} label: {
Label("已读", systemImage: "envelope.open")
}
.tint(.blue)
}
}
}
6.4 Section — 分组
Section 将列表内容分为多个命名区域,常用于设置页面和分类列表。
List {
// 带标题和底部注释的分组
Section {
Text("通知")
Text("声音")
Text("振动")
} header: {
Text("提醒设置") // 组标题(自动大写)
} footer: {
Text("通知将在勿扰模式下被屏蔽")
.font(.caption)
.foregroundColor(.secondary)
}
Section("账号") { // 简写:直接传字符串标题
Text("修改密码")
Text("注销登录")
}
}
.listStyle(.insetGrouped) // 圆角分组样式(最常见于设置页)
6.5 Form — 表单
Form 是专为设置页面设计的容器,外观类似 List(insetGrouped 样式),但对表单控件(Toggle、TextField、Picker 等)有更好的默认样式支持。
struct UserProfileForm: View {
@State private var name = ""
@State private var email = ""
@State private var bio = ""
@State private var isPublic = true
@State private var gender = "保密"
@State private var birthday = Date()
let genders = ["男", "女", "保密"]
var body: some View {
Form {
Section("基本信息") {
TextField("姓名", text: $name)
TextField("邮箱", text: $email)
.keyboardType(.emailAddress) // 邮件键盘
.textInputAutocapitalization(.never) // 不自动大写
}
Section("个人简介") {
TextField("写点什么...", text: $bio, axis: .vertical)
.lineLimit(3...6) // 多行自动扩展(iOS 16+)
}
Section("隐私") {
Toggle("公开主页", isOn: $isPublic)
Picker("性别", selection: $gender) {
ForEach(genders, id: \.self) { Text($0) }
}
DatePicker("生日", selection: $birthday,
displayedComponents: .date)
}
}
.navigationTitle("编辑资料")
}
}
6.6 TextField — 文本输入
TextField 是单行文本输入框,配合不同的 keyboardType 可以唤起对应类型的键盘,提升用户输入效率。
@State private var phone = ""
@State private var price = ""
@State private var url = ""
@State private var password = ""
// 电话号码键盘
TextField("手机号", text: $phone)
.keyboardType(.phonePad)
// 数字小键盘
TextField("价格", text: $price)
.keyboardType(.decimalPad) // 带小数点
// .keyboardType(.numberPad) — 纯数字,无小数点
// URL 键盘(带 .com 按钮)
TextField("网址", text: $url)
.keyboardType(.URL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
// 密码输入(隐藏字符)
SecureField("密码", text: $password)
// keyboardType 常用值:
// .default — 默认键盘
// .emailAddress — 邮件键盘(含 @ 和 .)
// .numberPad — 纯数字
// .decimalPad — 数字 + 小数点
// .phonePad — 电话号码
// .URL — URL 键盘
// .twitter — 含 @ 和 # 的键盘
// textFieldStyle:输入框外观
TextField("搜索", text: $phone)
.textFieldStyle(.roundedBorder) // 圆角边框
// .textFieldStyle(.plain) — 无边框
// onSubmit:按下回车/完成时触发
TextField("搜索", text: $phone)
.onSubmit {
print("用户提交:\(phone)")
}
.submitLabel(.search) // 键盘右下角按钮文字:search/done/go/next/send
// 多行文本输入(iOS 16+:axis: .vertical)
TextField("评论...", text: $url, axis: .vertical)
.lineLimit(1...5) // 最少1行,最多5行,自动扩展
6.7 常用输入控件
Toggle — 开关
@State private var isEnabled = true
@State private var useLocation = false
Toggle("启用通知", isOn: $isEnabled)
// 自定义样式
Toggle(isOn: $useLocation) {
Label("位置服务", systemImage: "location.fill")
}
.toggleStyle(.switch) // .switch(默认)/ .button / .checkbox(macOS)
// button 样式:点击整行切换
Toggle("夜间模式", isOn: $isEnabled)
.toggleStyle(.button)
.tint(.indigo)
Slider — 滑块
@State private var volume: Double = 0.5
@State private var fontSize: Double = 16
// 基本滑块
Slider(value: $volume, in: 0...1)
// 带步进的滑块 + 标签
Slider(
value: $fontSize,
in: 12...36,
step: 1 // 每次移动 1 个单位
) {
Text("字体大小") // 无障碍标签
} minimumValueLabel: {
Text("小").font(.caption)
} maximumValueLabel: {
Text("大").font(.caption)
}
Text("当前大小:\(Int(fontSize))pt")
Stepper — 步进器
@State private var quantity = 1
@State private var hours = 8
// 基本步进器(默认步进 1)
Stepper("数量:\(quantity)", value: $quantity, in: 1...99)
// 自定义步进值
Stepper("工时:\(hours) 小时",
value: $hours,
in: 1...24,
step: 2) // 每次加减 2
// 自定义操作
Stepper {
Text("值:\(quantity)")
} onIncrement: {
quantity = min(quantity + 1, 10)
// 自定义增加逻辑
} onDecrement: {
quantity = max(quantity - 1, 1)
// 自定义减少逻辑
}
DatePicker — 日期选择器
@State private var selectedDate = Date()
@State private var meetingTime = Date()
// 完整日期时间选择
DatePicker("选择日期", selection: $selectedDate)
// 仅选择日期
DatePicker("日期", selection: $selectedDate,
displayedComponents: .date)
// 仅选择时间
DatePicker("时间", selection: $meetingTime,
displayedComponents: .hourAndMinute)
// 限制日期范围
DatePicker("出发日期", selection: $selectedDate,
in: Date()..., // 只能选今天和之后
displayedComponents: .date)
// datePickerStyle
DatePicker("日期", selection: $selectedDate)
.datePickerStyle(.graphical) // 日历图形样式(最直观)
// .datePickerStyle(.wheel) — 滚轮样式
// .datePickerStyle(.compact) — 紧凑样式(默认,点击展开)
Picker — 选择器
@State private var selectedFruit = "苹果"
@State private var selectedIndex = 0
let fruits = ["苹果", "香蕉", "橙子"]
// 基本选择器(在 Form/List 中默认跳转新页面选择)
Picker("水果", selection: $selectedFruit) {
ForEach(fruits, id: \.self) { fruit in
Text(fruit).tag(fruit) // .tag() 对应 selection 的值
}
}
// pickerStyle:显示样式
Picker("水果", selection: $selectedFruit) {
ForEach(fruits, id: \.self) { Text($0).tag($0) }
}
.pickerStyle(.segmented) // 分段控件(常用于筛选栏)
// .pickerStyle(.wheel) — 滚轮
// .pickerStyle(.menu) — 下拉菜单
// .pickerStyle(.inline) — 内联列表
// .pickerStyle(.navigationLink) — 跳转页面(Form 默认)
// Picker 支持枚举(遵循 CaseIterable)
enum Theme: String, CaseIterable, Identifiable {
case light = "浅色"
case dark = "深色"
case system = "跟随系统"
var id: Self { self }
}
@State private var theme = Theme.system
Picker("主题", selection: $theme) {
ForEach(Theme.allCases) { t in
Text(t.rawValue).tag(t)
}
}
.pickerStyle(.segmented)
6.8 搜索功能:.searchable()
.searchable() 修饰符为视图添加系统样式的搜索栏,通常应用在 NavigationStack 内部的视图上,搜索栏会自动出现在导航栏下方。
struct SearchableListView: View {
let allFruits = ["苹果", "香蕉", "橙子", "葡萄", "草莓", "西瓜", "菠萝"]
@State private var searchText = ""
// 计算属性:根据搜索词过滤结果
var filteredFruits: [String] {
if searchText.isEmpty {
return allFruits
}
return allFruits.filter { $0.contains(searchText) }
}
var body: some View {
List(filteredFruits, id: \.self) { fruit in
Text(fruit)
}
.navigationTitle("水果")
// searchable:绑定搜索文本
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "搜索水果..." // 占位符
)
// placement 选项:
// .automatic — 自动(默认)
// .navigationBarDrawer(displayMode:) — 导航栏下拉
// .sidebar — 侧边栏(iPad)
// .toolbar — 工具栏
}
}
// 搜索建议(iOS 16+)
struct SearchWithSuggestionsView: View {
@State private var searchText = ""
let suggestions = ["Swift", "SwiftUI", "Xcode", "iOS"]
var body: some View {
List { /* 内容 */ }
.searchable(text: $searchText)
.searchSuggestions {
// 搜索建议列表
ForEach(suggestions.filter { $0.lowercased().contains(searchText.lowercased()) },
id: \.self) { suggestion in
Text(suggestion)
.searchCompletion(suggestion) // 点击后填入搜索框
}
}
}
}
6.9 实战示例:Todo List 应用
综合本章所有知识,构建一个完整的 Todo List App,包含添加、删除、完成标记、搜索和优先级分组功能。
// ─── 数据模型 ───
enum Priority: String, CaseIterable, Identifiable, Comparable {
case high = "高"
case medium = "中"
case low = "低"
var id: Self { self }
var color: Color {
switch self {
case .high: return .red
case .medium: return .orange
case .low: return .blue
}
}
static func < (lhs: Priority, rhs: Priority) -> Bool {
let order: [Priority] = [.high, .medium, .low]
return order.firstIndex(of: lhs)! < order.firstIndex(of: rhs)!
}
}
struct TodoItem: Identifiable {
let id = UUID()
var title: String
var isDone: Bool = false
var priority: Priority = .medium
var dueDate: Date = Date()
}
// ─── 添加 Todo 的 Sheet ───
struct AddTodoSheet: View {
@Environment(\.dismiss) var dismiss
var onAdd: (TodoItem) -> Void
@State private var title = ""
@State private var priority = Priority.medium
@State private var dueDate = Date()
var body: some View {
NavigationStack {
Form {
Section("任务内容") {
TextField("输入任务标题...", text: $title)
}
Section("任务属性") {
Picker("优先级", selection: $priority) {
ForEach(Priority.allCases) { p in
Label(p.rawValue, systemImage: "flag.fill")
.foregroundColor(p.color)
.tag(p)
}
}
DatePicker("截止日期", selection: $dueDate,
displayedComponents: .date)
}
}
.navigationTitle("新建任务")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("取消") { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("添加") {
let newTodo = TodoItem(
title: title,
priority: priority,
dueDate: dueDate
)
onAdd(newTodo)
dismiss()
}
.disabled(title.trimmingCharacters(in: .whitespaces).isEmpty)
}
}
}
}
}
// ─── Todo 列表行 ───
struct TodoRow: View {
let item: TodoItem
var onToggle: () -> Void
var body: some View {
HStack(spacing: 12) {
// 完成状态按钮
Button(action: onToggle) {
Image(systemName: item.isDone
? "checkmark.circle.fill"
: "circle")
.font(.title2)
.foregroundColor(item.isDone ? .green : .gray)
}
.buttonStyle(.plain) // 防止点击触发 NavigationLink
VStack(alignment: .leading, spacing: 2) {
Text(item.title)
.strikethrough(item.isDone) // 完成后加删除线
.foregroundColor(item.isDone ? .secondary : .primary)
HStack(spacing: 6) {
Image(systemName: "flag.fill")
.foregroundColor(item.priority.color)
.font(.caption)
Text(item.priority.rawValue)
.font(.caption)
.foregroundColor(item.priority.color)
Text("·").foregroundColor(.secondary)
Text(item.dueDate, style: .date)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.opacity(item.isDone ? 0.6 : 1.0)
}
}
// ─── 主视图 ───
struct TodoListView: View {
@State private var todos: [TodoItem] = [
TodoItem(title: "学习 SwiftUI 布局", priority: .high),
TodoItem(title: "完成状态管理章节", priority: .high),
TodoItem(title: "阅读官方文档", priority: .medium),
TodoItem(title: "写一个练习项目", priority: .medium),
TodoItem(title: "整理笔记", priority: .low, isDone: true),
]
@State private var searchText = ""
@State private var showAddSheet = false
@State private var showDoneItems = true
@State private var showDeleteDoneAlert = false
// 过滤后的待办列表
var pendingTodos: [TodoItem] {
todos.filter { !$0.isDone && (searchText.isEmpty || $0.title.contains(searchText)) }
.sorted { $0.priority < $1.priority }
}
// 已完成列表
var doneTodos: [TodoItem] {
todos.filter { $0.isDone && (searchText.isEmpty || $0.title.contains(searchText)) }
}
var body: some View {
NavigationStack {
List {
// 待办分组
Section {
ForEach(pendingTodos) { todo in
TodoRow(item: todo) {
toggleTodo(id: todo.id)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deleteTodo(id: todo.id)
} label: {
Label("删除", systemImage: "trash")
}
}
}
.onDelete { indexSet in
// 支持系统滑动删除
for index in indexSet {
deleteTodo(id: pendingTodos[index].id)
}
}
} header: {
HStack {
Text("待完成")
Spacer()
Text("\(pendingTodos.count) 项")
.foregroundColor(.secondary)
}
}
// 已完成分组(可折叠)
if !doneTodos.isEmpty {
Section {
if showDoneItems {
ForEach(doneTodos) { todo in
TodoRow(item: todo) {
toggleTodo(id: todo.id)
}
}
}
} header: {
Button {
showDoneItems.toggle()
} label: {
HStack {
Text("已完成 (\(doneTodos.count))")
Spacer()
Image(systemName: showDoneItems ? "chevron.down" : "chevron.right")
}
.font(.subheadline)
}
.buttonStyle(.plain)
.foregroundColor(.secondary)
} footer: {
if showDoneItems && !doneTodos.isEmpty {
Button("清除已完成任务") {
showDeleteDoneAlert = true
}
.foregroundColor(.red)
}
}
}
}
.navigationTitle("我的任务")
.searchable(text: $searchText, prompt: "搜索任务...")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showAddSheet = true
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showAddSheet) {
AddTodoSheet { newTodo in
todos.append(newTodo)
}
}
.alert("清除已完成", isPresented: $showDeleteDoneAlert) {
Button("清除", role: .destructive) {
todos.removeAll { $0.isDone }
}
Button("取消", role: .cancel) { }
} message: {
Text("将删除所有已完成的任务,此操作不可撤销。")
}
}
}
// ─── 辅助方法 ───
func toggleTodo(id: UUID) {
if let index = todos.firstIndex(where: { $0.id == id }) {
todos[index].isDone.toggle()
}
}
func deleteTodo(id: UUID) {
todos.removeAll { $0.id == id }
}
}
本章学习了 SwiftUI 的数据展示与交互输入体系:List 展示数据列表,ForEach+Identifiable 驱动动态内容,onDelete/onMove 实现编辑操作,Section 分组组织内容,Form 构建设置页面,以及 TextField、Toggle、Slider、Stepper、DatePicker、Picker 等输入控件的使用,最后通过 .searchable() 快速实现搜索功能。