6-7.【架构设计】SwiftUI 中 View 是值类型,但 UI 却是“持续存在的”,这是如何成立的?

1 阅读3分钟

一句话结论(先给直觉)

SwiftUI 的 View 不是 UI 本身,而是“如何构建 UI 的说明书(blueprint)”;
真正“持续存在”的,是 SwiftUI 在幕后维护的一棵 UI 状态树(渲染树)。

所以:

  • View值类型、随时可以被丢弃
  • UI 的持续性来自 State + Identity + Diff 算法,而不是 View 实例

一、先把最大的误解拆掉

❌ 错误直觉(UIKit 思维)

View struct = 屏幕上的那个 UI

✅ SwiftUI 的真实模型

View struct = 描述 UI 的函数 / 配方
UI 实体 = SwiftUI Runtime 管理的 Render Tree

你写的:

struct ContentView: View {
    var body: some View {
        Text("Hello")
    }
}

并不是在“创建一个 Text 对象”,而是在说:

“当前状态下,UI 应该长这样。”


二、SwiftUI 真正“持续存在”的是什么?

不是 View,而是 三样东西

1️⃣ State(状态)

@State var count: Int
@StateObject var viewModel
  • State 不存放在 View struct 里
  • SwiftUI Runtime 把它们存到一个 独立的状态存储(State Store)

即使 View struct 被销毁、重建:

View struct ❌ 重建
State Store ✅ 继续存在

2️⃣ Identity(身份)

SwiftUI 通过 identity 知道:

“这是同一个 UI 元素,还是一个新的?”

来源包括:

  • View 在树中的位置
  • .id(...)
  • ForEach 的 id
  • @StateObject 的创建点

这解决了一个关键问题:

“我这次生成的 View,是不是上一次那个?”


3️⃣ Render Tree(渲染树)

SwiftUI 在幕后维护一棵 引用类型的 UI 树

RenderNode (Text)
RenderNode (Button)
RenderNode (VStack)
  • 类似 UIKit 的 UIView
  • 但你 永远不直接接触
  • SwiftUI 负责 diff & 更新

三、状态变化时发生了什么?(关键流程)

假设你有:

struct CounterView: View {
    @State var count = 0

    var body: some View {
        Text("(count)")
        Button("+") { count += 1 }
    }
}

当你点按钮:

① State 改变

State Store: count = 01

② SwiftUI 重新“调用 body”

重新生成 CounterView(新的 struct 值)

⚠️ 注意:

不是“更新 View”,而是“重新计算 View 描述”

③ Diff(差分)

SwiftUI 比较:

上一次 View 描述
vs
这一次 View 描述

发现:

  • Text 的内容变了
  • Button 没变

④ 最小化更新 Render Tree

只更新 Text 对应的 RenderNode

👉 UI 看起来“持续存在”,但其实是 精准替换


四、为什么用值类型反而是优势?

1️⃣ View = 纯函数

View = f(State)
  • 没有隐藏副作用
  • 没有“半更新”状态
  • 可以无限次重建

这直接解决了 UIKit 中最痛苦的问题:

❌ “现在 UI 是不是和状态一致?”


2️⃣ 没有生命周期陷阱

在 SwiftUI 中:

  • 没有 viewDidLoad
  • 没有 viewWillAppear
  • 没有“这个 VC 现在活没活着”

因为:

View 本身不承载生命周期,State 才承载


3️⃣ 极强的可预测性(TCA 的根基)

这也是为什么 TCA / Redux 能在 SwiftUI 中如此自然:

Action → State → View

如果 View 是引用类型、可变对象:

  • 单一 State 就失效
  • 时间旅行就做不到

五、那 @StateObject 为什么“只创建一次”?

这正是 identity 在起作用。

@StateObject var vm = ViewModel()

SwiftUI 规则是:

相同 View identity 位置@StateObject 只初始化一次

  • View struct 重建 ❌
  • ViewModel 实例保留 ✅

这也是为什么你 不能 在子 View 中随便 new ViewModel —— identity 会变。


六、一个类比(非常重要)

把 SwiftUI 想成这样:

  • View = React / Flutter 里的 Widget
  • body = render 函数
  • State = 外部 Store
  • Render Tree = UIKit / AppKit 对象树

你写的不是 UI 本体,而是:

“状态 → UI 的映射关系”


七、最终总结(一句话版)

SwiftUI 中 UI 的“持续存在”,不是因为 View 活着,而是因为 State + Identity + Render Tree 活着;
View 只是一个随时可被丢弃、随时可重建的描述值。

如果你愿意,下一步我可以帮你:

  • SwiftUI vs UIKit 生命周期对照表,一眼打通直觉
  • 解释 为什么 SwiftUI 里不能在 View 里做副作用
  • 或从 TCA / 单向数据流 角度再推导一遍这个模型