在 SwiftUI 中,这四个属性包装器(Property Wrappers)构成了状态管理的基石。理解它们的关键在于两个维度:谁负责创建它(所有权) 以及 它能活多久(生命周期) 。
我们可以把它们分为两大阵营: “生产者”与“消费者” 。
1. 生产者阵营:我创建,我拥有
在这个阵营中,View 是数据的主人。即使 View 的 struct 被销毁重建,SwiftUI 也会在后台保护这些数据。
@State (值类型管家)
- 所有权:View 自己拥有。
- 生命周期:与 View 的**身份(Identity)**绑定。只要 View 在渲染树中没变,数据就一直都在。
- 适用场景:简单的私有状态,如开关、字符串、简单结构体。
- 防御点:必须标记为
private,防止外部干预。
@StateObject (引用类型管家)
- 所有权:View 拥有这个实例。
- 生命周期:这是关键! 它只会被初始化一次。即使 View 重复执行
init(),SwiftUI 也会复用第一次创建的那个实例。 - 适用场景:在当前 View 中初始化的 ViewModel 或数据管理器。
2. 消费者阵营:我使用,但不拥有
在这个阵营中,View 只是借用数据。数据由外部传入,View 仅负责监听变化并重绘。
@ObservedObject (临时引用)
- 所有权:View 不拥有它。
- 生命周期:随 View 的
init()而重连。 - 致命陷阱:如果你在 View 内部
init一个ObservedObject,每次父视图刷新导致当前 View 重新init时,你的对象都会被重置。 - 适用场景:父视图传给子视图的 ViewModel。
@EnvironmentObject (共享引用)
- 所有权:由祖先视图拥有(通常由祖先通过
.environmentObject()注入)。 - 生命周期:只要注入它的那个祖先还活着,它就活着。
- 适用场景:跨层级共享的数据,如用户信息、主题设置、全局设置。
- 防御点:如果忘记在祖先节点注入,App 会在运行时崩溃。
3. 生命周期与所有权对比表
| 包装器 | 数据类型 | 谁创建? | 随 View 重绘而重置? | 防御式建议 |
|---|---|---|---|---|
| @State | 值类型 (Struct) | 当前 View | 否 (SwiftUI 保护) | 仅用于局部 UI 逻辑 |
| @StateObject | 引用类型 (Class) | 当前 View | 否 (仅初始化一次) | 创建对象时必选 |
| @ObservedObject | 引用类型 (Class) | 外部传入 | 是 (若在内部 init) | 传递对象时必选 |
| @EnvironmentObject | 引用类型 (Class) | 祖先注入 | 否 | 用于全局/深层状态 |
4. 黄金准则:如何选?
- 如果你在 View 里
init一个类,请用@StateObject。 - 如果你从父视图 接收 一个类,请用
@ObservedObject。 - 如果你需要 跨越 3 层以上 的视图传递数据,请用
@EnvironmentObject。 - 如果是 简单的标志位 或 UI 内部状态,请用
@State。
总结
记住一句话: @StateObject 是为了“创造”,而 @ObservedObject 是为了“感知”。 如果在错误的地方使用了 @ObservedObject 来初始化对象,你会发现你的数据会在 UI 刷新时莫名其妙地重置为初始状态。