本节摘要:前四章用命令式伺候界面——造视图、改属性、手动刷新。SwiftUI 换世界观:你只声明"给定这份数据,界面长什么样",状态变化后的同步全部交给框架。本节用同一个浇水需求写出两种范式的完整对照,拆解声明式的三步机制(状态、描述、差量更新),并诚实列出声明式欠的账——它不是免费的。
需求很简单:点一下"浇水"按钮,倒计时标签从"还剩 2 天"变成"已浇水"。第 3 章的命令式写法你已经很熟了——每个变化都要亲手指挥:
// 命令式:UIKit——我指挥每一步 final class WaterVC: UIViewController { private let label = UILabel() private let button = UIButton(type: .system) private var passedDays = 2 // 状态藏在控制器属性里 private let interval = 3 @objc private func waterTapped() { passedDays = 0 // 第一步:改状态 label.text = "已浇水" // 第二步:亲自改标签 label.textColor = .systemGreen // 第三步:亲自改颜色 button.isEnabled = false // 第四步:亲自禁用按钮 button.setTitle("已浇水", for: .normal) // 第五步:亲自改按钮标题 } // 隐藏成本:明天还要加"环变绿、副标题更新"——回到这里继续加第六步第七步 }
SwiftUI 版:
// 声明式:SwiftUI——我只描述"状态对应的样子" import SwiftUI struct WaterView: View { @State private var passedDays = 2 // 状态:唯一的真相源 let interval = 3 var body: some View { // body 是"状态 → 界面"的函数 VStack(spacing: 16) { Text(passedDays >= interval ? "该浇水了" : passedDays == 0 ? "已浇水" : "还剩 \(interval - passedDays) 天") // 文案:现在时态描述 .foregroundColor(passedDays == 0 ? .green : .primary) Button(passedDays == 0 ? "已浇水" : "浇水") { passedDays = 0 // 唯一要做的事:改状态 } .disabled(passedDays == 0) // 禁用条件也描述出来 } } } // 点击后:passedDays 变 0 → 框架重算 body → 与旧树求差 → 只更新变化的那几处 // 界面效果与命令式版完全一致,代码里没有任何一步"亲自改界面"
数一数:命令式里每新增一个界面反应就多一行指挥代码;声明式里界面反应写在描述里,状态一变自动生效。改界面的工作没有消失,只是从你的代码搬进了框架。

声明式的一切神秘都能拆回三步。第一步状态:@State 标记的属性是视图的唯一真相源——这个标签为什么显示"已浇水"?因为 passedDays 是零,没有第二个答案。第二步描述:body 是个计算属性(2.4 的概念),每次访问都按当前状态现算出"界面应该长什么样"的描述树——它不是界面本身,是图纸。第三步差量:框架拿新图纸与旧图纸对比,只把变化的节点同步到屏幕。
用一段代码把"body 是函数"这件事看穿:
struct CounterView: View { @State private var count = 0 var body: some View { VStack { Text("浇了 \(count) 次") Button("记一次") { count += 1 } } } } // 每次点按钮: // count 变化 → 框架重新求值 body → 得到新的 Text("浇了 1 次") → 差量替换那一处文字 // 控制台验证(在按钮动作里加 print): // 点击前 count = 0 // 点击后 count = 1 界面文字同步变成"浇了 1 次"
对照第 3 章的思维:UIKit 里"界面是名词"(造好摆在那,逐步修);SwiftUI 里"界面是动词"(状态到图纸的映射,随叫随算)。第 1 章那十行 UIKit 与十行 SwiftUI 的对比,此刻应该真正看懂了。
背景:第 4 章反复强调"改了模型必须 reload",团队真实事故是改了数据忘刷表格。操作:把植物列表搬到 SwiftUI(完整版在 5.5),先看行视图的状态驱动版。
struct PlantRow: View { let plant: Plant // 数据进门(值语义,2.4 的红利) var body: some View { HStack { Circle().fill(plant.shouldWater ? Color.red : Color.green).frame(width: 10, height: 10) Text(plant.name) Spacer() // 撑开,把后面的文字推到右边 Text(plant.statusText).foregroundColor(.secondary) } } } // 使用侧一旦 plant 被新值替换,整行自动按新数据重画——不存在"忘了刷新"这个 bug 类别
解读:值语义在这里兑出现金——Plant 是结构体,替换即"新数据",框架比对新旧值决定重画,引用类型就做不到这么干净(2.5 讲过身份共享的陷阱)。变式:若行内还要"点按钮改这株植物",状态提升与绑定是 5.3 的主题;本节只要接受一个设定:描述树是状态的影子,影子永远跟着本体。
💡 关键直觉:写 SwiftUI 时问自己"这个界面是哪份数据的函数"。答得出来,架构就对了;答不出来,状态多半放错了层。
图纸由谁执笔、怎么读——下一节拆 View 协议与修饰符链。