在 SwiftUI 中, @State 和 @Binding 都和视图状态有关, 但它们负责的事情不一样。
简单说:
@State表示当前 View 自己拥有的一份状态@Binding表示当前 View 不拥有状态, 只是读写外部传进来的状态
如果把这个边界分清楚, 很多组件拆分时的数据流问题会简单很多。
@State: 状态由当前视图持有
当一个值只服务于当前视图, 并且不需要被父视图直接管理时, 可以使用 @State。
1 | struct CounterView: View { |
这里的 count 是 CounterView 自己的内部状态。按钮点击后修改 count, SwiftUI 会重新计算 body, 页面随之刷新。
@State 通常适合这些场景:
- 当前页面的临时 UI 状态, 例如展开/收起、选中项、输入框内容
- 不需要被父级直接读取的局部状态
- 视图销毁后可以自然丢弃的状态
需要注意的是, @State 一般声明为 private, 因为它代表当前视图自己的状态。如果外部需要控制它, 就应该考虑改成 @Binding。
@Binding: 状态由外部持有
当子视图需要修改父视图的状态时, 子视图不应该再创建一份新的 @State, 而应该通过 @Binding 接收状态引用。
1 | struct ParentView: View { |
这里 isOn 的真实数据存放在 ParentView 里, ToggleRow 只是通过 @Binding 拿到读写入口。
传值时使用 $isOn, 表示传递的是绑定关系, 而不是普通的 Bool 值。
@Binding 通常适合这些场景:
- 子视图需要修改父视图状态
- 封装输入控件、开关控件、选择器等组件
- 多个视图需要围绕同一份状态联动
常见误区: 子视图重新定义 State
有时候拆组件时, 容易在子视图里重新写一份 @State:
1 | struct ParentView: View { |
这段代码的问题是: NameEditor 里的 name 已经变成了子视图自己的状态。输入框修改后, 父视图里的 name 不会同步变化。
如果期望子视图修改父视图数据, 应该写成:
1 | struct ParentView: View { |
这样数据来源仍然只有一份, 子视图只是把修改动作传回父视图。
判断方式
可以用一个问题来判断:
这份状态到底归谁管理?
如果归当前视图管理, 用 @State。
如果归父视图或外部管理, 当前视图只负责展示和修改, 用 @Binding。
在组件封装中, 尽量让状态所有权保持清晰。谁创建状态, 谁负责决定状态的生命周期; 谁只是使用状态, 谁就通过绑定拿到读写能力。
一个简单封装例子
下面是一个可复用的搜索输入组件:
1 | struct SearchBar: View { |
外部使用:
1 | struct ContentView: View { |
SearchBar 不关心 keyword 的生命周期, 它只负责展示输入框和清空按钮。真正的数据由 ContentView 管理。
总结
@State 和 @Binding 的区别, 本质是状态所有权的区别。
@State: 我有这份状态@Binding: 我借用外部状态
写 SwiftUI 组件时, 先想清楚状态放在哪一层, 再决定用哪个属性包装器。这样页面拆分之后, 数据流会更稳定, 组件职责也会更清楚。