SwiftUI 本质论:声明式、可组合、状态驱动
zhezhongyun 2025-09-29 15:52 2 浏览
SwiftUI 本质论:声明式、可组合、状态驱动
引言:为什么是 SwiftUI,为什么是现在?
SwiftUI 的目标非常直接:用最短路径把你带到“优秀 App”。这个最短路径并不是偷工减料,而是让系统“替你干对的事”:
- o 原生控件 + 自适配(暗黑模式、动态字体、不同输入方式)。
- o 一套声明式语法表达你想要的结果,由框架完成如何实现。
- o 把你从“增删 cell / 手写状态同步 / 兼容多平台差异”的琐事里解放出来,把更多精力投入到独特功能与体验打磨。
要点回顾
o SwiftUI 提供控件(Button/Toggle/Picker)、容器(Stack/List/Form)、绘制、动画、手势。o 把平台特性语义化(macOS 菜单、watchOS 数字表冠、tvOS Siri Remote)。o 不是“Write Once, Run Anywhere”,而是“Learn Once, Apply Anywhere”。
目录
- 1. SwiftUI 的三根柱子:声明式 / 可组合 / 状态驱动
- 2. 从容器开始:VStack/HStack/ZStack/List/Form 与 ViewBuilder
- 3. 修饰符(Modifier)链:顺序即层级,语义即结果
- 4. 数据流入门:@State 与 @Binding 的双人舞
- 5. 控件的“语义化适配”:Button / Toggle / Picker 深入
- 6. 无障碍与环境(Accessibility & Environment):把“上下文”变成一等公民
- 7. 导航与结构:NavigationStack / NavigationSplitView / TabView(以及旧 API 兼容)
- 8. 跨平台要点:iOS / macOS / watchOS 的差异与统一
- 9. 完整实战:牛油果吐司订购 App(表单 + 历史 + 放蛋位拖拽 + 导航)
- 10. 最佳实践十条 & 常见坑排查
- 11. 思考题
- 12. 知识小结(TL;DR)
1. SwiftUI 的三根柱子:声明式 / 可组合 / 状态驱动
1.1 声明式(Declarative)
你描述 UI 的“是什么”,由 SwiftUI 负责**“怎么做”**。不再增/删 subview、不再手动 diff 列表,也不用为默认动画操心;你只需要保证状态正确。
类比:做牛油果吐司
- o 命令式:电话指导朋友每一步(取面包→烤→切牛油果…稍有失误全盘皆输)。
- o 声明式:找加州的牛油果工匠,说出期望与偏好,专家搞定细节。
1.2 可组合(Compositional)
用小视图搭大界面:容器(Stack/List/Form)+ 修饰符(Modifier)叠加。视图是值类型 struct,仅是“描述”。SwiftUI 内部有高效结构负责渲染与手势,拆分更多小组件几乎没有性能负担。
1.3 状态驱动(State-Driven)
UI = f(state)。谁被读取,谁是依赖;依赖改变,SwiftUI 重算 body 并最小化更新输出(屏幕/手势/可访问性)。
2. 从容器开始:ViewBuilder 与层级结构
容器视图(如 VStack/HStack/ZStack、List、Form)通过 @ViewBuilder 闭包声明孩子视图;你写出来的层级直观映射为 UI 结构。
# Swift
import SwiftUI
struct SimpleForm: View {
@State private var includeSalt = true
@State private var quantity = 1
var body: some View {
Form { // 与 VStack 同为容器,但对“表单”语义更友好
Section("基本配置") {
Toggle("加入盐", isOn: $includeSalt)
Stepper("数量:\(quantity)", value: $quantity, in: 1...6)
}
Section {
Button("下单") { /* 提交 */ }
}
}
.navigationTitle("牛油果吐司")
}
}
注意:容器切换(VStack→Form)无需改变内部控件定义;系统会按上下文自动适配背景、分隔线、按压态等细节。
3. 修饰符(Modifier)链:顺序就是层级
修饰符是“返回新视图的函数”,围绕基础视图包裹一层层“外衣”。
顺序决定层级与效果:
- o Text("Hi").padding().background(.green) → 背景包住文字+内边距
- o Text("Hi").background(.green).padding() → 绿色仅包住文字,外面再加空白
# Swift
struct ModifierOrderDemo: View {
var body: some View {
VStack(spacing: 16) {
Text("背景包住 padding")
.padding()
.background(.green.opacity(0.2))
Text("背景只包住文字")
.background(.green.opacity(0.2))
.padding()
}
.padding()
}
}
实践建议
- o 把“可复用的修饰符串”封装为自定义 ViewModifier,统一风格、减少重复。
- o 需要连贯动画时,尽量“把条件放进修饰符的参数”,而不是 if 切换不同视图类型(避免不必要的淡入淡出)。
4. 数据流入门:@State与@Binding的双人舞
4.1@State:视图的内部状态
由 SwiftUI 持有存储;本视图读写;改变即重算 body。
# Swift
struct Counter: View {
@State private var count = 0
var body: some View {
VStack {
Text("\(count)").font(.largeTitle).contentTransition(.numericText())
HStack {
Button("-") { withAnimation { count -= 1 } }
Button("+") { withAnimation { count += 1 } }
}
.buttonStyle(.borderedProminent)
}
}
}
4.2@Binding:双向引用外部状态
父视图拥有“事实来源”,子视图通过 Binding 读写。
# Swift
struct StepperEditor: View {
@Binding var value: Int
var body: some View {
Stepper("数量:\(value)", value: $value, in: 1...6)
}
}
struct Host: View {
@State private var qty = 1
var body: some View {
StepperEditor(value: $qty) // 传入 Binding
}
}
记忆:本地数据用 @State;父子共享用 @Binding;跨层共享再上升到 ObservableObject 家族(详见延伸阅读)。
5. 控件的“语义化适配”:Button / Toggle / Picker
5.1 Button:动作 + 标签
语义是“带标签的动作”,因此在不同上下文(滑动、菜单、表单)自动“换装”,但含义不变。
# Swift
Button {
// 动作:提交订单
} label: {
Label("下单", systemImage: "cart")
}
.buttonStyle(.borderedProminent) // 可换 .borderless / .bordered / .plain / 自定义 style
.tint(.green)
5.2 Toggle:开/关 + 标签(带Binding)
不仅外观可适配(开关/复选/切换按钮),而且自动接入辅助功能(VoiceOver 会读出标签和状态)。
# Swift
@State private var includeSalt = true
Toggle("加入盐", isOn: $includeSalt)
5.3 Picker:选项集合 + 选择值 + 标签
selection 是 Binding;选项用 ForEach 数据驱动;样式可在不同平台/上下文自适配或手动指定。
# Swift
enum Bread: String, CaseIterable, Identifiable { case sourdough, bagel, brioche
var id: Self { self }
}
@State private var bread: Bread = .sourdough
Picker("面包", selection: $bread) {
ForEach(Bread.allCases) { kind in
Text(kind.rawValue.capitalized).tag(kind)
}
}
// iOS 表单中默认“导航式”选择;也可强制 wheel 或 segmented
.pickerStyle(.menu) // .segmented / .inline / .wheel / .navigationLink 等
为什么 SwiftUI 控件“少而精”?
控件围绕“目的/角色”定义,而不是“外观”。在语义不变的前提下,自适配不同平台与场景,API 面更小更稳定,你也更容易迁移复用。
6. 无障碍(Accessibility)与环境(Environment)
6.1 无障碍基础
- o 为非文本标签提供 .accessibilityLabel 或使用 Label。
- o VoiceOver / Voice Control 会自动读出控件的目的与状态(因其语义化定义)。
# Swift
Image("egg")
.accessibilityLabel("水波蛋")
6.2 环境(Environment):把上下文注入为“一等公民”
环境值描述“视图所处环境”(布局方向、色彩方案、是否启用、地区语言等)。
- o 视图继承父环境;
- o 你可 .environment(\.xxx, value) 覆写子树;
- o 自定义视图也可读环境,自动响应变化。
# Swift
struct EggPlacement: View {
@Environment(\.isEnabled) private var isEnabled // 读取是否可交互
@State private var offset: CGSize = .zero
var body: some View {
ZStack {
Image("toast")
Image("egg")
.offset(offset)
.gesture(
DragGesture().onChanged { offset = $0.translation }
)
.saturation(isEnabled ? 1.0 : 0.0) // 禁用时去饱和
}
.frame(height: 220)
}
}
// 一键禁用子树(含手势)
Form { /* ... */ }
.disabled(true)
7. 导航与结构:NavigationStack/NavigationSplitView/TabView
API 更新提示
o 早年示例里出现的 NavigationView、NavigationLink(字幕里甚至有旧称“Navigationbutton”)在 iOS 16+ 逐步过渡到新的**NavigationStack/NavigationSplitView** 架构与值类型导航。o 若你的 minOS 较新,优先学习并使用 NavigationStack。
7.1 单栈导航:NavigationStack + NavigationLink
# Swift
struct Root: View {
var body: some View {
NavigationStack {
List(0..<10) { i in
NavigationLink("订单 #\(i)") {
OrderDetail(id: i)
}
}
.navigationTitle("订单历史")
}
}
}
7.2 分栏导航:NavigationSplitView(iPad/macOS 最佳)
# Swift
struct SplitRoot: View {
@State private var selection: Int?
var body: some View {
NavigationSplitView {
List(0..<50, selection: $selection) { i in
Text("订单 #\(i)")
}
} detail: {
if let id = selection {
OrderDetail(id: id)
} else {
Text("请选择订单")
}
}
}
}
7.3 Tab 结构
# Swift
TabView {
OrderFormView()
.tabItem { Label("下单", systemImage: "cart") }
OrderHistoryView()
.tabItem { Label("历史", systemImage: "clock") }
}
8. 跨平台要点:iOS / macOS / watchOS 的差异与统一
- o iOS:表单风格(Form)+ 导航行式 Picker。
- o macOS:信息密度更高;Picker 常见 menu/radioGroup;窗口与菜单特性丰富。
- o watchOS:旋转表冠 digitalCrownRotation 交互、密度极简。
- o 共同点:控件“语义化”定义,保证“学一次、到处用”。
# Swift (watchOS 示例)
struct CrownStepper: View {
@State private var value: Double = 3
var body: some View {
Text("分数:\(Int(value))")
.digitalCrownRotation($value, from: 0, through: 10, by: 1)
.focusable() // Watch 交互聚焦
}
}
9. 完整实战:牛油果吐司订购 App
目标:
o 表单下单(面包/抹酱/加盐/数量);o 选择“是否加蛋”与拖拽放置位置;o 订单历史列表 & 详情;o Tab 结构 + 栈式导航;o 可访问性/禁用态/环境值;o 现代 API(NavigationStack)。
9.1 模型与枚举
# Swift
import SwiftUI
enum Bread: String, CaseIterable, Identifiable {
case sourdough, bagel, brioche
var id: Self { self }
var label: String { switch self {
case .sourdough: return "酸面包"
case .bagel: return "贝果"
case .brioche: return "布里欧修"
}}
}
enum Spread: String, CaseIterable, Identifiable {
case butter, hummus, creamCheese, peanut
var id: Self { self }
var label: String { ["黄油","鹰嘴豆泥","奶油奶酪","花生酱"][Self.allCases.firstIndex(of: self)!] }
}
struct Order: Identifiable, Hashable {
let id: UUID = .init()
var bread: Bread
var spread: Spread
var includeSalt: Bool
var includeEgg: Bool
var eggOffset: CGSize? // 蛋位置(可选)
var quantity: Int
var time: Date = .init()
var summary: String {
"\(bread.label) + \(spread.label) \(includeSalt ? "加盐" : "无盐") ×\(quantity)" + (includeEgg ? " + 鸡蛋" : "")
}
}
9.2 订单状态 Store(演示用@State,可扩展ObservableObject)
# Swift
@MainActor
final class OrderStore: ObservableObject {
@Published var current = Order(
bread: .sourdough,
spread: .butter,
includeSalt: true,
includeEgg: false,
eggOffset: nil,
quantity: 1
)
@Published var history: [Order] = []
func submit() {
history.insert(current, at: 0)
// 重置当前订单(保留部分偏好)
current.quantity = 1
current.includeEgg = false
current.eggOffset = nil
}
}
9.3 放蛋位拖拽视图
# Swift
struct EggPlacementView: View {
@Environment(\.isEnabled) private var isEnabled
@Binding var offset: CGSize?
var body: some View {
ZStack {
Image("toast").resizable().scaledToFit()
Image("egg")
.resizable().scaledToFit()
.frame(width: 60, height: 60)
.offset(offset ?? .zero)
.gesture(
DragGesture().onChanged { gesture in
offset = gesture.translation
}
)
.saturation(isEnabled ? 1 : 0) // 被禁用时视觉降饱和
.accessibilityLabel("水波蛋位置")
}
.frame(height: 220)
.padding(.vertical)
}
}
提示:
o .disabled(true) 能“一键”禁用手势与控件;o 自定义视图可通过环境读取 isEnabled 生成视觉反馈。
9.4 下单表单
# Swift
struct OrderFormView: View {
@EnvironmentObject var store: OrderStore
@State private var networkOK = true // 演示禁用态:假设网络状态
var body: some View {
Form {
Section("基本配置") {
Picker("面包", selection: $store.current.bread) {
ForEach(Bread.allCases) { b in Text(b.label).tag(b) }
}
Picker("抹酱", selection: $store.current.spread) {
ForEach(Spread.allCases) { s in Text(s.label).tag(s) }
}
Toggle("加入盐", isOn: $store.current.includeSalt)
Stepper("数量:\(store.current.quantity)", value: $store.current.quantity, in: 1...6)
}
Section("加鸡蛋") {
Toggle("需要鸡蛋", isOn: $store.current.includeEgg.animation()) // 动画插入行
if store.current.includeEgg {
NavigationLink("放置位置") {
EggPlacementView(offset: $store.current.eggOffset)
.navigationTitle("放置鸡蛋")
.toolbar {
Button("归位") { store.current.eggOffset = .zero }
}
}
}
}
Section {
Button {
store.submit()
} label: {
Label("下单", systemImage: "cart.fill")
}
.buttonStyle(.borderedProminent)
.tint(.green)
.disabled(!networkOK || store.current.quantity == 0)
} footer: {
Text(networkOK ? "下单将保存到历史" : "网络不可用,表单已禁用")
}
}
.navigationTitle("牛油果吐司")
.accentColor(.green) // 可作用于整棵子树
.disabled(!networkOK && true) // 演示:一键禁用整棵表单(含手势)
.toolbar {
Button {
networkOK.toggle()
} label: {
Label(networkOK ? "断网" : "联网", systemImage: networkOK ? "wifi.slash" : "wifi")
}
}
}
}
9.5 历史列表与详情
# Swift
struct OrderHistoryView: View {
@EnvironmentObject var store: OrderStore
var body: some View {
List {
ForEach(store.history) { order in
NavigationLink(value: order) {
HStack {
VStack(alignment: .leading) {
Text(order.summary)
Text(order.time, style: .time).font(.caption).foregroundStyle(.secondary)
}
Spacer()
if order.includeEgg { Image("egg").resizable().frame(width: 20, height: 20) }
}
.accessibilityElement(children: .combine)
.accessibilityLabel("订单 \(order.summary)")
}
}
}
.navigationTitle("订单历史")
}
}
struct OrderDetail: View {
let order: Order
var body: some View {
VStack(spacing: 16) {
Text(order.summary).font(.title3)
EggPlacementView(offset: .constant(order.eggOffset))
.disabled(true)
Spacer()
}
.padding()
.navigationTitle("订单详情")
}
}
9.6 应用入口:Tab + Stack 导航
# Swift
@main
struct AvocadoToastApp: App {
@StateObject private var store = OrderStore()
var body: some Scene {
WindowGroup {
NavigationStack {
TabView {
OrderFormView()
.tabItem { Label("下单", systemImage: "cart") }
OrderHistoryView()
.tabItem { Label("历史", systemImage: "clock") }
}
.navigationDestination(for: Order.self) { OrderDetail(order: $0) }
}
.environmentObject(store)
}
}
}
运行提示:将 Assets 添加 toast 与 egg 两张图片;无图也可将图片换成 SF Symbols(例如 Image(systemName: "frying.pan"))以快速运行。
10. 最佳实践十条 & 常见坑排查
10.1 最佳实践
- 1. 小组件化:OrderRow、EggPlacementView、StepperEditor 等拆小,复用、预览、测试都更顺手。
- 2. 把条件放进修饰符参数:需要连续动画/过渡时,尽量不要用 if 切视图类型。
- 3. 单一事实来源:局部 @State → 父子 @Binding → 跨层 ObservableObject。避免状态复制。
- 4. 预览先行:为关键视图写多种 #Preview(深色/大字/RTL),减少真机反复跑。
- 5. 列表 id 稳定:Identifiable 或 .id 保证稳定,避免闪烁与错误动画。
- 6. 动画粒度:withAnimation{} 包裹改变状态的地方,小步快跑,避免整页抖动。
- 7. 环境驱动:优先用 Environment 与 .disabled/.tint/.accentColor 等向下渗透策略。
- 8. 导航用新栈:能用 NavigationStack/NavigationSplitView 就不要新项目里继续 NavigationView。
- 9. 平台分支:必要时用 #if os(macOS) 等分支,逻辑与 UI 解耦。
- 10. 互操作边界清晰:需要 UIKit 控件时用 UIViewRepresentable 包装,生命周期集中在 Coordinator。
10.2 常见坑
- o “改了数据 UI 不刷”:确认该数据是否在 body 被读取;ObservableObject 记得 @Published;拥有者用 @StateObject。
- o “Binding 传错”:子视图参数是 @Binding,父级需传 $state,而非拷贝值。
- o “动画变成淡入淡出”:你很可能 if 切换了两种不同视图类型;把条件移入 modifier 的参数。
- o “列表刷新错位”:id 不稳定或重复;或对数据做了非等价替换。
- o “预览总编不过”:缺资源/条件编译;先用最小依赖跑通预览,再逐项打开。
11. 知识小结
- o 声明式:描述“结果”,由框架“实现过程”;UI = f(state)。
- o 可组合:小视图 + 容器 + 修饰符;视图是值类型,拆分不损性能。
- o 状态驱动:@State(本地)→ @Binding(父子)→ ObservableObject(跨层)。
- o 修饰符链:顺序即层级;善用自定义 ViewModifier 复用风格。
- o 控件语义化:Button/Toggle/Picker 在不同上下文自适配;你专注“目的”,系统负责“外观”。
- o 无障碍与环境:标签/状态天然可被 VoiceOver 等读取;Environment 让上下文传递与覆写变容易。
- o 导航现代化:NavigationStack / NavigationSplitView + TabView 组合,覆盖 iPhone/iPad/macOS。
- o 跨平台:Learn Once, Apply Anywhere;必要处做平台分支与样式覆写。
- o 迁移策略:从新功能或单页开始引入;UIKit 控件可互嵌;保持所有权与状态边界清晰。
相关推荐
- VSCode中值得推荐的常用的23个高效前端插件(工具篇)(一)
-
VSCode是我们前端开发的一个强大的IDE,所以选择趁手好用的插件是提高开发效率,然后剩下的时间用来摸鱼是很有必要滴。工具篇(23)Chinese(Simplified)vscode我们都知道是...
- 高级前端进阶,用gulp提升你的开发效率
-
前言:这两天动手配置了一下gulp,发现gulp配置简单,构建速度快,在某些使用场景下还是个不错的选择,本文从零开始构建,到最后打包发布到生成环境。通过本文可以快速上手gulp,文末附送github源...
- Chrome 110 3大新特性!CSS支持画中画!
-
大家好,很高兴又见面了,我是"前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天带着大家一起看看最新发布的Chrome1...
- 用html中If语句——判断ie浏览器的版本
-
if语句的代码的语法非常简单,,就是一个if判断语句来判断浏览器的类型和版本,应用类似<!--[iflteIE6]>和<![endif]-->语法结构包孕起来...
- 谷歌浏览器怎么开启无痕浏览_谷歌浏览器怎么开启无痕浏览模式
-
很多用户在使用谷歌浏览器时,不希望留下任何上痕迹,开启无痕浏览器是最好的选择。这个模式下可以更好的保护个人隐私记录,给你带来更加安全的冲浪体验,接下来就给大家详细介绍下谷歌浏览器的无痕浏览模式,希望对...
- Linux命令那么多,其实只需要记住这些就足够了!
-
你好,这里是网络技术联盟站,我是瑞哥。Linux命令行是一个强大且灵活的工具,可以极大地提高用户的工作效率和系统管理能力。我们都知道,Linux命令非常多,但是在实际的工作中,日常使用到的命令并不多,...
- Linux如何查看文件_linux如何查看文件大小
-
Linux如何查看目录下的所有文件?用ls(list)查看当前目录下的所有文件和子目录。Ls查看目录下的文件,怎么区分是目录还是文件呢?第一种方式,我们可以通过颜色来区分目录和文件。默认情况下,目录显...
- Linux系统man命令使用详解_linux man命令详解
-
man命令是在Linux和Unix系统上用于查看系统手册页(manualpages)的工具。手册页提供了关于系统命令、函数和文件的详细文档。命令语法:man[选项][命令或主题]参数:[选项]...
- linux ps命令详解_linux中ps
-
linux中ps只显示进程的静态快照,及瞬间的进程状态,它拥有众多的风格,可分为3组:UNIX风格,BSD风格,GNU风格,本文介绍UNIX风格的ps指令。参数ps[-aefFly][-ppid...
- 如何在 Linux 上查找系统硬件信息?hwinfo命令很强大!
-
hwinfo是一个功能强大的硬件信息查询工具,专为Linux系统设计。它能够提供系统中几乎所有硬件组件的详细信息,包括但不限于CPU、内存、硬盘、网络设备、USB设备、显卡、声卡等。与其他常...
- Linux Shell 入门教程(二):常用命令大全与使用技巧
-
在上一节《理解Linux与Shell》中,我们了解了Linux是什么、Shell是什么以及常见的Shell类型。这一篇,我们将正式动手操作,掌握使用频率最高、最实用的Linux命令...
- SpringBoot应用部署神器:可视化服务管理脚本让运维更轻松
-
在SpringBoot应用的生产环境部署中,传统的手动启停服务方式不仅效率低下,还容易出错。今天分享一个功能强大的可视化服务管理脚本,让SpringBoot应用的部署和运维变得简单高效。痛点分析:传统...
- 一次虚拟机性能问题导致的应用故障
-
最近我负责维护的一套语音平台出了问题。故障现象据客户反馈是转入IVR以后没有正常响应,客户无奈挂机了。老实说,刚开始接到用户反馈的时候,我是不太相信的。我们的系统平时运行运行很稳定,客户的并发数不大,...
- linux中的常用命令_linux常用命令及含义
-
linux中的常用命令linux中的命令统称shell命令shell是一个命令行解释器,将用户命令解析为操作系统所能理解的指令,实现用户与操作系统的交互shell终端:我们平时输入命令,执行程序的那个...
- linux学习笔记——常用命令-文件处理命令
-
ls目录处理命令:ls全名:list命令路径:/bin/ls执行权限:所有用户ls–ala--alll–long-i查看i节点ls–i查看i节点命令名称:mkdir命令英文原意:m...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)
- opacity 属性 (32)
- transition 属性 (33)