为什么"状态"是 SwiftUI 的牛顿第三定律?
在物理学里,力与反作用力成对出现;在 SwiftUI 里,状态变化与UI 反应也成对出现。
用户每一次点击、每一次网络返回,都相当于给系统施加了一个"力",而 UI 必须以某种"反作用力"做出响应。
因此,状态管理不是可选技能,而是 SwiftUI 世界的万有引力。
最小状态原则(Minimal State)
先记住一句"咒语": "能让 UI 正确且及时响应的最少状态到底是哪些?"
凡是不在这份清单里的数据,一律:
- 计算得出 → 用 computed property
- 可推导 → 不用 @State
- 临时存活 → 用 let 或局部变量
这样做的好处:
- 减少无效刷新,提升性能
- 降低心智负担,代码更易读
- 为后续拆分模块、复用组件扫清障碍
状态载具速查表
| 属性包装器 | 作用域 | 典型用途 | 备注 |
|---|---|---|---|
@State |
当前 View 私有 | 局部 UI 小数据(如 String、Bool、Int) | 值类型 |
@Binding |
父子共享 | 将"引用"传给子 View,使其能修改父数据 | 不拥有数据 |
@StateObject |
当前 View 私有 | 创建并持有引用类型(如 ObservableObject) | 生命周期与 View 一致 |
@ObservedObject |
任意 View | 外部传入的 ObservableObject | 不创建,只引用 |
@Environment |
全局 | 系统级值(如 colorScheme、locale 等) | 通过 key 读取 |
@EnvironmentObject |
全局 | 自定义共享对象 | 需提前注入 |
本文不讨论 MVVM / MVC / TCA 等架构,只聚焦"状态本身如何存在、如何流动"。
从 0 到 1:状态是如何"被声明"的?
静态单状态 ------ 宇宙奇点
swift
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
注释:没有 @State,UI 永远不变,宇宙一片死寂。
引入第一个 @State ------ 宇宙大爆炸
swift
struct ContentView: View {
@State private var statefulText: String = "Stateful Text"
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text(statefulText) // 依赖状态
}
.padding()
}
}
注释:现在 UI 可以随 statefulText 变化而刷新,但用户还没法干预。
让用户当"上帝"------ 引入交互
swift
struct ContentView: View {
@State private var statefulText: String = "Stateful Text"
var body: some View {
VStack {
Button {
statefulText = "Ouch!" // 用户施加"力"
} label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
}
Text(statefulText) // 自动反应
}
.padding()
}
}
注释:状态变化 ⇄ UI 反应 的环路闭合,宇宙开始演化。
多状态宇宙:枚举扛起大旗
把"加载中/加载成功/错误/哲学生命周期"抽象成枚举,一次性把状态机搬进 UI:
swift
// 1. 定义状态机
enum ViewState {
case loading
case loaded
case error
case whoami
}
struct ContentView: View {
// 2. 最小状态:当前处于哪一步
@State private var currentState: ViewState = .loading
// 3. 仅 loaded 才关心的文字
@State private var statefulText: String = "We did it!"
var body: some View {
Group {
switch currentState {
case .loading:
ContentUnavailableView("One moment please...",
systemImage: "hourglass")
case .loaded:
loadedUI
case .error:
ContentUnavailableView("Oops!",
systemImage: "x.circle")
case .whoami:
Text("Existential Crisis!")
}
}
.task {
// 4. 模拟网络请求
do {
try await Task.sleep(nanoseconds: 2_000_000_000)
currentState = .loaded
} catch {
currentState = .error
}
}
}
// 把 loaded 状态 UI 拆成 computed property,可读性更好
private var loadedUI: some View {
VStack {
Button("点我改文字") {
statefulText = "Ouch!"
}
Button("进入哲学模式") {
currentState = .whoami
}
Text(statefulText)
}
.padding()
}
}
注释:
- 用
switch做穷尽式匹配,编译器帮你检查漏掉的状态 .task修饰符在视图出现时自动执行,离开即取消,比onAppear更安全
状态树:像公司一样分部门治理
当父 View 既要管"加载枚举"又要管"文字细节"时,责任过重。
把只跟 loaded 相关的状态下放给子 View,父级只保留"导航级"状态,形成状态树:
swift
struct ContentView: View {
@State private var currentState: ViewState = .loading
var body: some View {
Group {
switch currentState {
case .loading:
ContentUnavailableView("One moment please...",
systemImage: "hourglass")
case .loaded:
LoadedView(currentState: $currentState) // 只传绑定
case .error:
ContentUnavailableView("Oops!",
systemImage: "x.circle")
case .whoami:
Text("Existential Crisis!")
}
}
.task {
do {
try await Task.sleep(nanoseconds: 2_000_000_000)
currentState = .loaded
} catch {
currentState = .error
}
}
}
}
// 子 View:只关心 loaded 世界
struct LoadedView: View {
@Binding var currentState: ViewState // 需要回跳父级,用 Binding
@State private var statefulText: String = "We did it!" // 局部状态
var body: some View {
VStack(spacing: 20) {
Button("改文字") {
statefulText = "Ouch!"
}
Button("哲学模式") {
currentState = .whoami
}
Text(statefulText)
.font(.title)
}
.padding()
}
}
注释:
- 父 View 代码量瞬间减半,职责单一
- 子 View 可独立预览、独立测试,甚至一键抽成 Swift Package给别的项目用
递归地追问"最小状态"
状态树不是一层就够。如果 LoadedView 里再出现子模块(比如点赞数、评论列表),继续问自己:
- 这些子模块是否必须由父级驱动?
- 能否把"点赞数"做成
@StateObject的LikesService,通过@EnvironmentObject注入? - 能否把"评论列表"做成纯粹
@State的局部数组,只在进入评论区才初始化?
每一层都回答一次"最小状态"问题,复杂度就被递归地压扁。
实战扩展:把状态树做成"可插拔"模块
假设未来要加"夜间模式"全局开关:
swift
@main
struct MyApp: App {
@StateObject private var theme = ThemeService() // 遵循 ObservableObject
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(theme) // 一次性注入
}
}
}
任意深层子 View 只需:
swift
struct DeepChild: View {
@EnvironmentObject var theme: ThemeService
var body: some View {
Text("深夜模式自动响应")
.foregroundColor(theme.labelColor)
}
}
关键点:
- 全局状态绝不放在根 View 的
@State,而是@StateObject+@EnvironmentObject - 业务层继续遵循"最小状态",不依赖全局主题即可独立运行
总结与 Checklist
- 先写"死"的 UI,再慢慢声明状态,而不是一上来就
@State满天飞。 - 每一次加状态前,背一遍咒语:"这是让 UI 正确且及时响应的最小集合吗?"
- 当状态超过 3 个且互相耦合,立刻画状态树:
- 谁能独立?→ 拆子 View
- 谁需共享?→ 拆 ObservableObject
- 谁只读?→ 用 Environment
- 把"枚举 + switch"当成状态机语法糖,穷尽所有 case,让编译器当你 QA。