SwiftUI为了让开发者快速构建类iOS设置页面的界面,提供了Form这个专门的容器组件。Form本质上是一种带分组的列表视图,它会自动为内部的每一行加上圆角卡片、分割线和合适的内边距,配合Picker、Toggle、DatePicker等控件,几乎不用写额外样式代码就能得到系统设置应用那样精致的表单效果。本文围绕Form与Picker的使用展开,覆盖日期选择、分段控制器和联动菜单三个最常见的场景。

Form容器的基本结构与行布局
Form的使用方式与List几乎一致,闭包内放置若干视图,每个视图自动成为一行。区别在于Form会应用分组的表单外观,背景变为系统灰色,行内容以白色圆角卡片分组展示。下面是一个典型的用户信息表单,包含文本输入、开关和按钮等常见元素:
struct UserProfileForm: View {
@State private var username = ""
@State private var notificationsEnabled = true
var body: some View {
NavigationView {
Form {
Section(header: Text("基本信息")) {
TextField("请输入用户名", text: $username)
Toggle("接收通知", isOn: $notificationsEnabled)
}
Section(header: Text("其他")) {
Button("保存设置") {
// 处理保存逻辑
}
}
}
.navigationTitle("个人资料")
}
}
}Section用于给表单分组,可以指定header和footer。header通常显示在分组上方的小标题文字,footer则显示在分组下方的说明性灰色文字,这两者都是系统自动排版的,不需要手动调整字体大小和颜色。需要注意的是,Form内部每一行的高度和布局会根据内容自动适配,如果想让一行内左右对齐,可以借助HStack和Spacer组合,系统不会强制你使用特定的行布局。
另一个实用细节是,Form继承自List,因此也支持onDelete、onMove等编辑能力。如果你的表单里有一个可增删的子列表,比如多个收货地址,直接在Section里使用ForEach并提供删除回调即可,无需额外引入其他组件。
Picker的五种样式与默认菜单行为
Picker是表单中最常用也最容易让人困惑的控件,因为它在不同环境下的默认表现不一样。Picker一共有五种常见样式:Automatic、Menu、Wheel、Segmented和Inline。当Picker单独放在普通View中时,默认呈现为类似UISegmentedControl的样式;但一旦放进Form或List中,默认样式会变成Menu,也就是点击后弹出底部菜单或下拉菜单,同时行内显示当前选中值的导航样式,这也是为什么把Picker放进Form后外观会突然改变的原因。
struct PickerDemo: View {
@State private var gender = "男"
let options = ["男", "女", "保密"]
var body: some View {
Form {
Section(header: Text("样式对比")) {
Picker("性别", selection: $gender) {
ForEach(options, id: \.self) { item in
Text(item)
}
}
// 自动样式:在Form内表现为菜单
Picker("性别", selection: $gender) {
ForEach(options, id: \.self) { item in
Text(item)
}
}
.pickerStyle(.wheel)
// 转轮样式,直接内嵌展开
Picker("性别", selection: $gender) {
ForEach(options, id: \.self) { item in
Text(item)
}
}
.pickerStyle(.segmented)
// 分段样式
}
}
}
}Menu样式适合选项较多的场景,点击后以浮层形式展示,不占用表单空间;Wheel样式是传统的滚轮,通常与DatePicker配合出现在页面上;Inline样式会把所有选项直接展开为一组单选行,在Form中表现为带对勾标记的分组列表,适合选项不多且希望用户一眼看到全部选项的情况。使用ForEach构造选项时,务必保证每个选项有唯一标识,可以直接用字符串的self作为id,也可以让数据模型遵循Identifiable协议。
DatePicker日期选择与格式化显示
DatePicker是SwiftUI中对UIDatePicker的封装,支持日期、时间以及日期时间组合三种模式。它天然支持本地化和深色模式,默认样式在Form中显示为一行,点击后展开系统的日历选择器。通过in参数可以限制可选的日期范围,这在预订、签到等业务中非常实用:
struct DatePickerDemo: View {
@State private var checkInDate = Date()
// 限制只能选择今天到未来30天之间
var dateRange: ClosedRange<Date> {
let calendar = Calendar.current
let start = calendar.startOfDay(for: Date())
let end = calendar.date(byAdding: .day, value: 30, to: start)!
return start...end
}
var body: some View {
Form {
Section(header: Text("入住日期")) {
DatePicker(
"选择日期",
selection: $checkInDate,
in: dateRange,
displayedComponents: [.date]
)
// 把选中的日期格式化后展示在另一行
HStack {
Text("当前选中")
Spacer()
Text(checkInDate, style: .date)
.foregroundColor(.secondary)
}
}
}
}
}展示日期时优先使用Text直接接收Date并传入style参数的方式,例如Text(checkInDate, style: .date),它会自动按照用户设备的地区设置显示对应格式,无需手动拼接。如果确实需要自定义格式,可以创建DateFormatter实例并设置dateFormat属性,比如yyyy年MM月dd日,但要注意本地化问题,硬编码格式在切换语言后可能出现奇怪的表现。另外DatePicker还提供datePickerStyle修饰符,graphical样式会直接展开完整的日历视图,compact样式则显示为紧凑的可点击文本,可根据页面空间灵活选择。
Segmented分段控制器的使用细节
分段控制器在SwiftUI中就是Picker加上segmented样式,常见于列表的筛选切换,比如订单页面的全部、待付款、已完成标签。它有几个使用细节值得注意:第一,选项文字过长时会自动缩小字体,若仍然放不下会截断,因此每个分段的文案尽量控制在四个字以内;第二,分段数量建议不超过五个,否则在窄屏设备上体验很差;第三,可以在选项中放入图标,Segmented样式支持Label作为子视图,系统会自动布局:
struct OrderFilterBar: View {
@State private var filterType = 0
var body: some View {
VStack(spacing: 0) {
Picker("筛选", selection: $filterType) {
Text("全部").tag(0)
Text("待付款").tag(1)
Text("已完成").tag(2)
}
.pickerStyle(.segmented)
.padding()
// 根据选择切换内容区域
switch filterType {
case 0: Text("显示全部订单列表")
case 1: Text("显示待付款订单")
default: Text("显示已完成订单")
}
Spacer()
}
}
}这里使用了tag修饰符把整数类型与选项绑定,selection绑定的类型必须与tag的类型一致,否则会出现选中状态不生效的隐蔽问题,这是初学者最常遇到的坑之一。另外分段控制器本身的背景色和选中色可以通过.tint()修饰符调整选中段的颜色,外观定制能力比UIKit时代的UISegmentedControl简洁很多。
省市联动菜单:状态驱动的级联选择
联动菜单是表单开发中的进阶需求,典型例子是先选省份再选城市,城市列表必须跟随省份变化。核心思路是:两个Picker分别绑定两个状态,当第一个Picker的值变化时,重置第二个状态并根据新省份刷新城市数据源。SwiftUI的声明式特性让这一切只需要关注数据关系,不需要手动刷新UI:
struct CascadePickerDemo: View {
// 省份与城市数据,实际项目中可来自接口
let data: [String: [String]] = [
"广东省": ["广州", "深圳", "珠海"],
"浙江省": ["杭州", "宁波", "温州"],
"四川省": ["成都", "绵阳", "乐山"]
]
@State private var province = "广东省"
@State private var city = "广州"
var cities: [String] {
data[province] ?? []
}
var body: some View {
Form {
Section(header: Text("所在地区")) {
Picker("省份", selection: $province) {
ForEach(data.keys.sorted(), id: \.self) { key in
Text(key).tag(key)
}
}
Picker("城市", selection: $city) {
ForEach(cities, id: \.self) { name in
Text(name).tag(name)
}
}
}
}
.onChange(of: province) { newValue in
// 省份变化时,把城市重置为该省第一个城市
// 避免出现选中的城市不属于当前省份的脏数据
if let first = data[newValue]?.first {
city = first
}
}
}
}这段代码中有两个关键点。一是城市数据源city作为计算属性直接由province派生,省份变化后城市列表自动更新,无需手动刷新;二是必须通过onChange监听省份变化并重置城市,否则用户先选了深圳再切换到浙江省时,selection中保留的深圳并不在新列表里,Picker会显示空白或出现绑定失效的问题。如果层级更多,比如省市区三级联动,原理相同,每一级都要监听上一级的变化并逐级重置下级状态。
对于数据量较大的联动场景,建议把数据模型定义为遵循Codable的结构体数组,通过id或code字段建立父子关系,而不是用字典,这样更便于接口解析和持久化。掌握Form的分组布局、Picker的样式切换以及onChange驱动的联动逻辑后,绝大多数设置页、筛选页和录入页都可以快速搭建完成。