Data Essentials in SwiftUI
Swift & UI 入门 26m

SwiftUI 数据驱动基础

Data Essentials in SwiftUI

2020年6月23日

在 Apple 官方观看视频

一句话判断

如果你对 @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 属性变化时手动触发更新
WWDC 2020