SwiftUI 本质论:声明式、可组合、状态驱动
zhezhongyun 2025-09-29 15:52 51 浏览
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 控件可互嵌;保持所有权与状态边界清晰。
相关推荐
- Python入门学习记录之一:变量_python怎么用变量
-
写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...
- python变量命名规则——来自小白的总结
-
python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...
- Python入门学习教程:第 2 章 变量与数据类型
-
2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...
- 绘制学术论文中的“三线表”具体指导
-
在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...
- Python基础语法知识--变量和数据类型
-
学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...
- 一文搞懂 Python 中的所有标点符号
-
反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...
- Python变量类型和运算符_python中变量的含义
-
别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...
- 从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序
-
在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...
- Python中下划线 ‘_’ 的用法,你知道几种
-
Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...
- 解锁Shell编程:变量_shell $变量
-
引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...
- 一文学会Python的变量命名规则!_python的变量命名有哪些要求
-
目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...
- 更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for
-
src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...
- C++第五课:变量的命名规则_c++中变量的命名规则
-
变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....
- Rust编程-核心篇-不安全编程_rust安全性
-
Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...
- 探秘 Python 内存管理:背后的神奇机制
-
在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...
- 一周热门
- 最近发表
- 标签列表
-
- 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)
- opacity 属性 (32)
- transition 属性 (33)
- 1-1. 变量声明 (31)
