SwiftUI 数据驱动基础
Data Essentials in SwiftUI
2020年6月23日
一句话判断
如果你对 @State、@Binding、@StateObject、@ObservedObject、@EnvironmentObject 之间的区别还拿不准,这场 Session 用清晰的示例逐一拆解——看完之后你不会再选错属性包装器。
这场 Session 讲了什么
SwiftUI 的数据流是它最核心的设计理念,也是初学者最容易困惑的地方。Session 从”数据依赖图”的概念出发,系统梳理了所有数据流相关的属性包装器。
核心逻辑是:View 是结构体(值类型),不能持有引用类型的状态。SwiftUI 提供了不同的属性包装器来解决这个问题。@State 用于 View 内部的简单值类型状态;@Binding 用于父子视图之间传递可读写的状态引用;@StateObject 用于在 View 中创建和拥有 ObservableObject(iOS 14 新增);@ObservedObject 用于引用外部传入的 ObservableObject;@EnvironmentObject 用于跨视图层级共享数据。
Session 还介绍了 2020 年新增的 @StateObject,解决了 @ObservedObject 在 View 重建时可能多次创建对象的问题。
值得深挖的点
@StateObject vs @ObservedObject 的关键区别。 @ObservedObject 不持有对象的生命周期——如果你在 View 的 init 中创建 ObservableObject 并赋值给 @ObservedObject,每次 View 重建时都会创建新对象,之前的状态就丢失了。@StateObject 在第一次初始化时创建对象,后续 View 重建时复用同一个实例。规则很简单:谁创建谁拥有,创建者用 @StateObject,接收者用 @ObservedObject。
值类型 vs 引用类型的数据流设计。 SwiftUI 的数据流围绕”单一数据源”原则:每个数据有且只有一个 owner。值类型(@State)通过 @Binding 共享,引用类型(ObservableObject)通过 @ObservedObject 或 @EnvironmentObject 共享。不要混用——不要把 @State 的值传给 ObservableObject 再传回来,这会破坏数据流的可追踪性。
代码片段
import SwiftUI
// @State——View 内部的简单状态
struct CounterView: View {
@State private var count = 0 // View 拥有这个状态
var body: some View {
VStack {
Text("\(count)")
.font(.largeTitle)
Button("增加") { count += 1 }
}
}
}
// @Binding——父子视图共享状态
struct StepperView: View {
@Binding var value: Int // 不拥有,只是引用父视图的状态
var body: some View {
HStack {
Button("-") { value -= 1 }
Text("\(value)")
Button("+") { value += 1 }
}
}
}
struct ParentView: View {
@State private var score = 0
var body: some View {
VStack {
StepperView(value: $score) // 通过 $ 前缀传递 Binding
}
}
}
// @StateObject vs @ObservedObject
class UserData: ObservableObject {
@Published var username = "匿名"
@Published var isLoggedIn = false
}
struct ProfileView: View {
@StateObject var userData = UserData() // 创建并拥有
// 正确:StateObject 保证只创建一次
var body: some View {
VStack {
Text("欢迎, \(userData.username)")
SettingsView(userData: userData) // 传递给子视图
}
}
}
struct SettingsView: View {
@ObservedObject var userData: UserData // 接收外部传入的对象
// 正确:不创建,只引用
var body: some View {
TextField("用户名", text: $userData.username)
}
}
// @EnvironmentObject——跨层级共享
@main
struct MyApp: App {
@StateObject var appData = AppData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appData) // 注入到环境中
}
}
}
// 任何子视图都可以获取
struct DeepChildView: View {
@EnvironmentObject var appData: AppData // 自动从环境中获取
var body: some View {
Text(appData.username)
}
}
最佳实践
- 简单状态用 @State,复杂状态用 @StateObject。 如果数据只有几个字段(如 toggle 状态、选中索引),@State 足够。如果数据有多个关联属性或需要跨多个视图共享,用 ObservableObject。
- 创建者用 @StateObject,接收者用 @ObservedObject。 这是 2020 年的新规则,替代了之前”都用 @ObservedObject”的旧做法。
- @EnvironmentObject 用于真正需要全局共享的数据。 如用户信息、App 设置。不要滥用——如果一个数据只有两个视图需要,用 @ObservedObject 传递更清晰。
- 永远不要在 ObservableObject 中使用 @State。 @State 只能用于 View 内部。
还有什么值得关注
@Published属性变化时会自动触发依赖视图的重新渲染@Environment可以读取系统值(如 colorScheme、locale、scenePhase 等)\.dismiss环境值可以用来在代码中关闭 Sheet- ObservableObject 的
objectWillChange发送器可以在非 @Published 属性变化时手动触发更新