控件和指示器(Controls and Indicators)
全面解析 SwiftUI 中的控件与指示器,涵盖按钮、开关、选择器、滑块、进度视图等所有交互组件的使用方法与最佳实践。
SwiftUI 提供了丰富的控件和指示器,用于处理用户交互和显示信息。这些组件针对不同平台和上下文进行了优化,能够帮助开发者快速构建直观、易用的用户界面。
概述
控件和指示器是 SwiftUI 中用于用户交互的核心组件集合。控件(Controls)用于接收用户输入和触发操作,而指示器(Indicators)用于向用户展示状态和进度信息。
主要分类:
- 按钮类控件:触发操作和命令
- 选择控件:从选项中进行选择
- 数值调整控件:调整数值范围
- 进度指示器:显示任务进度和状态
- 分组控件:组织和管理相关内容
- 链接控件:导航和分享
按钮类控件
Button
Button 是最基础的交互控件,用于触发操作。关于 Button 的详细内容,请参考专门的 Button 学习笔记。
基本用法:
Button("点击我") {
print("按钮被点击")
}
.buttonStyle(.borderedProminent)EditButton
EditButton 是一个特殊按钮,用于切换列表的编辑模式。
struct EditableListView: View {
@State private var items = ["项目 1", "项目 2", "项目 3"]
var body: some View {
NavigationStack {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
.onMove { source, destination in
items.move(fromOffsets: source, toOffset: destination)
}
}
.navigationTitle("可编辑列表")
.toolbar {
EditButton() // 自动切换编辑模式
}
}
}
}特点:
- 自动管理
editMode环境值 - 在编辑和完成状态之间切换
- 仅在 iOS、iPadOS 和 Mac Catalyst 上可用
PasteButton
PasteButton 提供系统级的粘贴功能,可以从剪贴板读取特定类型的数据。
struct PasteButtonExample: View {
@State private var pastedText = ""
var body: some View {
VStack(spacing: 20) {
Text("粘贴的内容:\(pastedText)")
.padding()
PasteButton(payloadType: String.self) { strings in
pastedText = strings.first ?? ""
}
}
}
}支持多种内容类型:
import UniformTypeIdentifiers
PasteButton(supportedContentTypes: [.plainText, .url]) { items in
for item in items {
item.loadItem(forTypeIdentifier: UTType.plainText.identifier) { data, error in
if let data = data as? Data,
let text = String(data: data, encoding: .utf8) {
print("粘贴的文本:\(text)")
}
}
}
}RenameButton
RenameButton 触发标准的重命名操作,配合 .renameAction() 修饰符使用。
struct RenameButtonExample: View {
@State private var itemName = "我的文档"
@FocusState private var isFocused: Bool
var body: some View {
NavigationStack {
VStack {
TextField("名称", text: $itemName)
.focused($isFocused)
.textFieldStyle(.roundedBorder)
.padding()
}
.navigationTitle("文档")
.toolbar {
RenameButton()
}
.renameAction {
isFocused = true
}
}
}
}链接控件
Link
Link 用于导航到 URL,会在系统默认浏览器中打开链接。
// 基本链接
Link("访问 Apple", destination: URL(string: "https://www.apple.com")!)
// 自定义样式
Link(destination: URL(string: "https://developer.apple.com")!) {
HStack {
Image(systemName: "safari")
Text("开发者文档")
}
.foregroundStyle(.blue)
}ShareLink
ShareLink 提供系统级的分享功能,可以分享文本、图片、URL 等内容。
struct ShareLinkExample: View {
let shareText = "查看这个很棒的应用!"
let shareURL = URL(string: "https://example.com")!
var body: some View {
VStack(spacing: 20) {
// 分享文本
ShareLink(item: shareText)
// 分享 URL
ShareLink(item: shareURL) {
Label("分享链接", systemImage: "square.and.arrow.up")
}
// 分享带预览
ShareLink(
item: shareURL,
subject: Text("精彩内容"),
message: Text("不要错过这个!")
)
}
}
}分享图片:
ShareLink(
item: Image("photo"),
preview: SharePreview(
"美丽的风景",
image: Image("photo")
)
)选择控件
Toggle
Toggle 是一个开关控件,用于在开启和关闭状态之间切换。
struct ToggleExample: View {
@State private var isOn = false
@State private var notifications = true
@State private var darkMode = false
var body: some View {
Form {
Section("设置") {
Toggle("启用功能", isOn: $isOn)
Toggle(isOn: $notifications) {
HStack {
Image(systemName: "bell.fill")
Text("推送通知")
}
}
Toggle("深色模式", systemImage: "moon.fill", isOn: $darkMode)
}
}
}
}自定义样式:
Toggle("自动保存", isOn: $isOn)
.toggleStyle(.switch) // 开关样式(默认)
.tint(.purple)
Toggle("同意条款", isOn: $agreed)
.toggleStyle(.button) // 按钮样式Picker
Picker 用于从一组互斥的选项中进行选择。
struct PickerExample: View {
@State private var selectedFruit = "苹果"
@State private var selectedColor = Color.red
let fruits = ["苹果", "香蕉", "橙子", "葡萄"]
var body: some View {
Form {
// 基本 Picker
Picker("选择水果", selection: $selectedFruit) {
ForEach(fruits, id: \.self) { fruit in
Text(fruit).tag(fruit)
}
}
// 分段控件样式
Picker("颜色", selection: $selectedColor) {
Text("红色").tag(Color.red)
Text("绿色").tag(Color.green)
Text("蓝色").tag(Color.blue)
}
.pickerStyle(.segmented)
}
}
}不同的 Picker 样式:
// 菜单样式
Picker("选项", selection: $selection) {
// ...
}
.pickerStyle(.menu)
// 轮盘样式(仅 iOS)
Picker("选项", selection: $selection) {
// ...
}
.pickerStyle(.wheel)
// 内联样式
Picker("选项", selection: $selection) {
// ...
}
.pickerStyle(.inline)
// 导航样式
Picker("选项", selection: $selection) {
// ...
}
.pickerStyle(.navigationLink)DatePicker
DatePicker 用于选择日期和时间。
struct DatePickerExample: View {
@State private var selectedDate = Date()
@State private var birthDate = Date()
@State private var meetingTime = Date()
var body: some View {
Form {
// 完整日期和时间
DatePicker("选择日期", selection: $selectedDate)
// 仅日期
DatePicker(
"生日",
selection: $birthDate,
displayedComponents: .date
)
// 仅时间
DatePicker(
"会议时间",
selection: $meetingTime,
displayedComponents: .hourAndMinute
)
// 限制日期范围
DatePicker(
"预约日期",
selection: $selectedDate,
in: Date()..., // 只能选择今天及以后
displayedComponents: .date
)
}
}
}紧凑样式:
DatePicker("日期", selection: $date)
.datePickerStyle(.compact)
DatePicker("日期", selection: $date)
.datePickerStyle(.graphical) // 图形化日历
DatePicker("日期", selection: $date)
.datePickerStyle(.wheel) // 轮盘样式MultiDatePicker
MultiDatePicker 允许选择多个日期。
struct MultiDatePickerExample: View {
@State private var selectedDates: Set<DateComponents> = []
var body: some View {
VStack {
MultiDatePicker("选择日期", selection: $selectedDates)
Text("已选择 \(selectedDates.count) 个日期")
.padding()
// 限制日期范围
MultiDatePicker(
"可用日期",
selection: $selectedDates,
in: Date()...
)
}
}
}ColorPicker
ColorPicker 用于选择颜色。
struct ColorPickerExample: View {
@State private var selectedColor = Color.blue
@State private var backgroundColor = Color.white
var body: some View {
VStack(spacing: 20) {
ColorPicker("选择颜色", selection: $selectedColor)
// 支持透明度
ColorPicker("背景色", selection: $backgroundColor, supportsOpacity: true)
Rectangle()
.fill(selectedColor)
.frame(height: 100)
.cornerRadius(10)
}
.padding()
}
}数值调整控件
Slider
Slider 用于在一个范围内选择数值。
struct SliderExample: View {
@State private var volume: Double = 50
@State private var brightness: Double = 0.5
@State private var temperature: Double = 20
var body: some View {
Form {
// 基本滑块
Slider(value: $volume, in: 0...100)
Text("音量:\(Int(volume))")
// 带标签
Slider(value: $brightness, in: 0...1) {
Text("亮度")
}
// 带最小/最大值标签
Slider(
value: $temperature,
in: 0...40,
step: 0.5
) {
Text("温度")
} minimumValueLabel: {
Text("0°")
} maximumValueLabel: {
Text("40°")
}
Text("当前温度:\(temperature, specifier: "%.1f")°C")
}
}
}监听编辑状态:
Slider(
value: $value,
in: 0...100,
onEditingChanged: { editing in
if editing {
print("开始拖动")
} else {
print("结束拖动,最终值:\(value)")
}
}
)Stepper
Stepper 用于通过增减按钮调整数值。
struct StepperExample: View {
@State private var quantity = 1
@State private var age = 18
@State private var price = 9.99
var body: some View {
Form {
// 基本步进器
Stepper("数量:\(quantity)", value: $quantity)
// 指定范围和步长
Stepper(
"年龄:\(age)",
value: $age,
in: 0...120,
step: 1
)
// 浮点数步进器
Stepper(
value: $price,
in: 0...100,
step: 0.5
) {
Text("价格:$\(price, specifier: "%.2f")")
}
// 自定义增减逻辑
Stepper("自定义") {
quantity += 5
} onDecrement: {
quantity = max(0, quantity - 5)
}
}
}
}进度指示器
ProgressView
ProgressView 用于显示任务的进度。
struct ProgressViewExample: View {
@State private var progress = 0.0
@State private var isLoading = false
var body: some View {
VStack(spacing: 30) {
// 不确定进度(旋转指示器)
ProgressView()
ProgressView("加载中...")
// 确定进度
ProgressView(value: progress, total: 100)
// 带标签和当前值
ProgressView(value: progress, total: 100) {
Text("下载进度")
} currentValueLabel: {
Text("\(Int(progress))%")
}
Button("开始") {
startProgress()
}
}
.padding()
}
func startProgress() {
progress = 0
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
progress += 1
if progress >= 100 {
timer.invalidate()
}
}
}
}自定义样式:
ProgressView(value: progress)
.progressViewStyle(.linear) // 线性样式
.tint(.purple)
ProgressView()
.progressViewStyle(.circular) // 圆形样式时间进度:
// 倒计时
ProgressView(
timerInterval: Date()...Date().addingTimeInterval(60),
countsDown: true
) {
Text("倒计时")
}Gauge
Gauge 用于在一个范围内显示数值,类似仪表盘。
struct GaugeExample: View {
@State private var speed: Double = 60
@State private var battery: Double = 75
@State private var temperature: Double = 22
var body: some View {
VStack(spacing: 30) {
// 基本仪表
Gauge(value: speed, in: 0...200) {
Text("速度")
}
// 带当前值标签
Gauge(value: battery, in: 0...100) {
Text("电量")
} currentValueLabel: {
Text("\(Int(battery))%")
}
// 带最小/最大值标签
Gauge(value: temperature, in: 0...40) {
Text("温度")
} currentValueLabel: {
Text("\(Int(temperature))°")
} minimumValueLabel: {
Text("0°")
} maximumValueLabel: {
Text("40°")
}
}
.padding()
}
}不同的 Gauge 样式:
// 线性容量样式
Gauge(value: storage, in: 0...100) {
Text("存储")
}
.gaugeStyle(.linearCapacity)
// 圆形样式(watchOS)
Gauge(value: heartRate, in: 0...200) {
Text("心率")
}
.gaugeStyle(.circular)
// 访问样式
Gauge(value: progress, in: 0...100) {
Text("进度")
}
.gaugeStyle(.accessoryCircular)分组控件
GroupBox
GroupBox 用于在视觉上将相关内容分组。
struct GroupBoxExample: View {
var body: some View {
VStack(spacing: 20) {
// 无标签分组
GroupBox {
Text("这是一组相关内容")
Text("它们被视觉上分组在一起")
}
// 带标签分组
GroupBox("用户信息") {
VStack(alignment: .leading, spacing: 10) {
Text("姓名:张三")
Text("邮箱:zhangsan@example.com")
Text("电话:123-456-7890")
}
}
// 自定义标签
GroupBox {
VStack(alignment: .leading, spacing: 10) {
Text("设置项 1")
Text("设置项 2")
Text("设置项 3")
}
} label: {
Label("高级设置", systemImage: "gear")
.font(.headline)
}
}
.padding()
}
}ControlGroup
ControlGroup 将语义相关的控件组合在一起,系统会根据上下文自动选择合适的显示方式。
struct ControlGroupExample: View {
@State private var isBold = false
@State private var isItalic = false
@State private var isUnderline = false
var body: some View {
VStack(spacing: 20) {
// 文本格式控件组
ControlGroup {
Button {
isBold.toggle()
} label: {
Image(systemName: "bold")
}
Button {
isItalic.toggle()
} label: {
Image(systemName: "italic")
}
Button {
isUnderline.toggle()
} label: {
Image(systemName: "underline")
}
}
// 带标签的控件组
ControlGroup("对齐方式") {
Button { } label: {
Image(systemName: "text.alignleft")
}
Button { } label: {
Image(systemName: "text.aligncenter")
}
Button { } label: {
Image(systemName: "text.alignright")
}
}
}
.padding()
}
}DisclosureGroup
DisclosureGroup 创建可折叠的内容区域。
struct DisclosureGroupExample: View {
@State private var isExpanded = false
@State private var showDetails = true
var body: some View {
VStack(spacing: 20) {
// 基本折叠组
DisclosureGroup("更多信息") {
Text("这是隐藏的详细内容")
Text("点击标题可以展开或折叠")
}
// 控制展开状态
DisclosureGroup(
"详细设置",
isExpanded: $showDetails
) {
Toggle("选项 1", isOn: .constant(true))
Toggle("选项 2", isOn: .constant(false))
Toggle("选项 3", isOn: .constant(true))
}
Button("切换展开") {
showDetails.toggle()
}
// 嵌套折叠组
DisclosureGroup("高级选项") {
DisclosureGroup("网络设置") {
Text("WiFi 设置")
Text("蓝牙设置")
}
DisclosureGroup("隐私设置") {
Text("位置服务")
Text("相机权限")
}
}
}
.padding()
}
}其他控件
LabeledContent
LabeledContent 用于将标签和值配对显示。
struct LabeledContentExample: View {
var body: some View {
Form {
Section("用户信息") {
LabeledContent("姓名", value: "张三")
LabeledContent("年龄", value: "28")
LabeledContent("邮箱", value: "zhangsan@example.com")
}
Section("统计数据") {
LabeledContent("总计") {
Text("¥1,234.56")
.foregroundStyle(.green)
.bold()
}
LabeledContent {
HStack {
Image(systemName: "star.fill")
Text("4.8")
}
.foregroundStyle(.orange)
} label: {
Text("评分")
}
}
}
}
}格式化数值:
let price = 99.99
let date = Date()
LabeledContent("价格", value: price, format: .currency(code: "USD"))
LabeledContent("日期", value: date, format: .dateTime)触觉反馈
使用 sensoryFeedback 为控件添加触觉反馈。
struct SensoryFeedbackExample: View {
@State private var isLiked = false
@State private var count = 0
var body: some View {
VStack(spacing: 30) {
Button {
isLiked.toggle()
} label: {
Image(systemName: isLiked ? "heart.fill" : "heart")
.font(.largeTitle)
.foregroundStyle(isLiked ? .red : .gray)
}
.sensoryFeedback(.impact, trigger: isLiked)
Button("增加") {
count += 1
}
.sensoryFeedback(.increase, trigger: count)
Button("成功操作") {
// 执行操作
}
.sensoryFeedback(.success, trigger: count)
}
}
}可用的反馈类型:
.sensoryFeedback(.success, trigger: value) // 成功
.sensoryFeedback(.warning, trigger: value) // 警告
.sensoryFeedback(.error, trigger: value) // 错误
.sensoryFeedback(.selection, trigger: value) // 选择
.sensoryFeedback(.increase, trigger: value) // 增加
.sensoryFeedback(.decrease, trigger: value) // 减少
.sensoryFeedback(.impact, trigger: value) // 冲击控件配置
控件尺寸
使用 controlSize 统一调整控件大小。
struct ControlSizeExample: View {
@State private var value = 50.0
var body: some View {
VStack(spacing: 30) {
VStack {
Text("迷你尺寸")
Slider(value: $value, in: 0...100)
Button("按钮") { }
.buttonStyle(.bordered)
}
.controlSize(.mini)
VStack {
Text("小尺寸")
Slider(value: $value, in: 0...100)
Button("按钮") { }
.buttonStyle(.bordered)
}
.controlSize(.small)
VStack {
Text("常规尺寸")
Slider(value: $value, in: 0...100)
Button("按钮") { }
.buttonStyle(.bordered)
}
.controlSize(.regular)
VStack {
Text("大尺寸")
Slider(value: $value, in: 0...100)
Button("按钮") { }
.buttonStyle(.bordered)
}
.controlSize(.large)
}
.padding()
}
}控件样式定制
大多数控件都支持通过样式协议进行自定义:
// Toggle 样式
struct CustomToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.label
Spacer()
RoundedRectangle(cornerRadius: 16)
.fill(configuration.isOn ? Color.green : Color.gray)
.frame(width: 50, height: 30)
.overlay {
Circle()
.fill(.white)
.padding(3)
.offset(x: configuration.isOn ? 10 : -10)
}
.onTapGesture {
withAnimation(.spring()) {
configuration.isOn.toggle()
}
}
}
}
}
// 使用
Toggle("自定义开关", isOn: $isOn)
.toggleStyle(CustomToggleStyle())实战示例
设置页面
struct SettingsView: View {
@State private var notifications = true
@State private var soundEnabled = true
@State private var volume: Double = 70
@State private var theme = "自动"
@State private var fontSize: Double = 16
let themes = ["浅色", "深色", "自动"]
var body: some View {
NavigationStack {
Form {
Section("通知") {
Toggle("推送通知", isOn: $notifications)
Toggle("声音", isOn: $soundEnabled)
if soundEnabled {
VStack(alignment: .leading) {
Text("音量")
Slider(value: $volume, in: 0...100)
Text("\(Int(volume))%")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
Section("外观") {
Picker("主题", selection: $theme) {
ForEach(themes, id: \.self) { theme in
Text(theme).tag(theme)
}
}
Stepper(
"字体大小:\(Int(fontSize))",
value: $fontSize,
in: 12...24,
step: 2
)
}
Section("关于") {
LabeledContent("版本", value: "1.0.0")
LabeledContent("构建号", value: "100")
}
}
.navigationTitle("设置")
}
}
}表单验证
struct FormValidationView: View {
@State private var name = ""
@State private var email = ""
@State private var age = 18
@State private var agreedToTerms = false
@State private var selectedCountry = "中国"
@State private var birthDate = Date()
let countries = ["中国", "美国", "英国", "日本"]
var isFormValid: Bool {
!name.isEmpty &&
!email.isEmpty &&
email.contains("@") &&
agreedToTerms
}
var body: some View {
NavigationStack {
Form {
Section("个人信息") {
TextField("姓名", text: $name)
TextField("邮箱", text: $email)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
Stepper("年龄:\(age)", value: $age, in: 1...120)
DatePicker(
"生日",
selection: $birthDate,
displayedComponents: .date
)
Picker("国家", selection: $selectedCountry) {
ForEach(countries, id: \.self) { country in
Text(country).tag(country)
}
}
}
Section {
Toggle("我同意服务条款", isOn: $agreedToTerms)
}
Section {
Button("提交") {
submitForm()
}
.frame(maxWidth: .infinity)
.disabled(!isFormValid)
}
}
.navigationTitle("注册")
}
}
func submitForm() {
print("表单已提交")
}
}下载管理器
struct DownloadManagerView: View {
@State private var downloads: [Download] = [
Download(name: "文件 1.pdf", progress: 0.3),
Download(name: "图片.jpg", progress: 0.7),
Download(name: "视频.mp4", progress: 0.5)
]
var body: some View {
NavigationStack {
List {
ForEach($downloads) { $download in
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(download.name)
.font(.headline)
Spacer()
Text("\(Int(download.progress * 100))%")
.font(.caption)
.foregroundStyle(.secondary)
}
ProgressView(value: download.progress)
HStack {
if download.progress < 1.0 {
Button("暂停") {
// 暂停下载
}
.buttonStyle(.bordered)
.controlSize(.small)
} else {
Label("完成", systemImage: "checkmark.circle.fill")
.foregroundStyle(.green)
}
Spacer()
Button("取消") {
cancelDownload(download)
}
.buttonStyle(.bordered)
.controlSize(.small)
.tint(.red)
}
}
.padding(.vertical, 8)
}
}
.navigationTitle("下载管理")
}
}
func cancelDownload(_ download: Download) {
downloads.removeAll { $0.id == download.id }
}
}
struct Download: Identifiable {
let id = UUID()
var name: String
var progress: Double
}平台差异
不同平台上控件的表现有所差异:
| 控件 | iOS/iPadOS | macOS | watchOS | tvOS |
|---|---|---|---|---|
| Button | 触摸交互 | 点击交互 | 数字表冠 | 遥控器 |
| Toggle | 开关样式 | 复选框 | 开关样式 | 不可用 |
| Picker | 多种样式 | 下拉菜单 | 轮盘样式 | 不可用 |
| Slider | 触摸拖动 | 鼠标拖动 | 数字表冠 | 不可用 |
| DatePicker | 轮盘/日历 | 日历弹窗 | 不可用 | 不可用 |
| ColorPicker | 颜色选择器 | 系统颜色面板 | 不可用 | 不可用 |
最佳实践
1. 选择合适的控件
// ✅ 推荐:二选一用 Toggle
Toggle("启用功能", isOn: $isEnabled)
// ❌ 不推荐:二选一用 Picker
Picker("功能", selection: $isEnabled) {
Text("启用").tag(true)
Text("禁用").tag(false)
}2. 提供清晰的标签
// ✅ 推荐:描述性标签
Slider(value: $brightness, in: 0...1) {
Text("屏幕亮度")
}
// ❌ 不推荐:无标签或模糊标签
Slider(value: $brightness, in: 0...1)3. 合理使用控件尺寸
// ✅ 推荐:根据上下文调整尺寸
VStack {
// 工具栏中的小按钮
HStack {
Button("编辑") { }
Button("删除") { }
}
.controlSize(.small)
// 主要操作使用大按钮
Button("提交") { }
.controlSize(.large)
}4. 提供即时反馈
// ✅ 推荐:显示当前值
Stepper("数量:\(quantity)", value: $quantity)
Slider(value: $volume, in: 0...100)
Text("音量:\(Int(volume))%")5. 考虑可访问性
Slider(value: $volume, in: 0...100)
.accessibilityLabel("音量")
.accessibilityValue("\(Int(volume))%")
Button(action: { }) {
Image(systemName: "trash")
}
.accessibilityLabel("删除")
.accessibilityHint("删除当前项目")总结
SwiftUI 的 Controls and Indicators 提供了完整的用户交互解决方案:
- 按钮类控件:处理用户操作和命令
- 选择控件:提供多种选择方式,适应不同场景
- 数值调整控件:精确或快速调整数值
- 进度指示器:清晰展示任务状态
- 分组控件:组织和管理复杂界面
- 配置选项:统一控制外观和行为
通过合理组合这些控件,可以构建出功能强大、体验优秀的用户界面。记住要根据具体场景选择合适的控件,提供清晰的标签和即时反馈,并始终考虑可访问性。
参考资源
上次更新于