WWDC Quick Look 💓 By SwiftGGTeam
Data Essentials in SwiftUI

Data Essentials in SwiftUI

观看原视频

Highlight

SwiftUI 的数据流是它最核心的设计理念,也是初学者最容易困惑的地方。Session 从”数据依赖图”的概念出发,系统梳理了所有数据流相关的属性包装器。


核心内容

SwiftUI 里的 View 是短生命周期的结构体。每次上层状态变化,SwiftUI 都可能重新创建一份 view struct,再用它生成新的界面。开发者如果把数据生命周期理解成 view struct 的生命周期,就会在刷新、导航、多窗口和异步加载时遇到状态丢失。

这场演讲给出一个固定问题:这个视图需要什么数据、会不会修改数据、数据从哪里来。只展示上层传入的数据时,用普通属性。当前视图拥有瞬时状态时,用 @State。子视图需要修改父视图的同一份状态时,用 @Binding

当数据已经进入模型层,@State 就不够了。演讲用一个阅读 App 说明:阅读进度、完成状态、封面加载和全局书单都有不同生命周期。SwiftUI 用 ObservableObject@ObservedObject@StateObject@EnvironmentObject@SceneStorage@AppStorage 把这些生命周期分开,让界面依赖数据,而不是手动监听事件。

最后一段重点是性能。body 应该只是描述界面,不能在里面创建昂贵对象或做阻塞工作。新引入的 @StateObject 解决了一个常见错误:在 view 初始化路径里反复创建 ObservableObject,导致慢更新和数据重置。


详细内容

@State 保存当前视图拥有的瞬时状态

05:09

演讲先从更新阅读进度的 sheet 开始。BookView 需要保存 sheet 是否展示、用户笔记和当前进度。因为这些字段一起变化,Apple 把它们收进一个值类型 EditorConfig,再交给 @State 管理。

struct EditorConfig {
    var isEditorPresented = false
    var note = ""
    var progress: Double = 0
    mutating func present(initialProgress: Double) {
        progress = initialProgress
        note = ""
        isEditorPresented = true
    }
}
struct BookView: View {
    @State private var editorConfig = EditorConfig()
    func presentEditor() { editorConfig.present() }
    var body: some View {

        Button(action: presentEditor) {  }

    }
}

关键点:

  • EditorConfig 把三个相关字段放在一起,进度、笔记和展示状态不会分散在多个属性里。
  • present(initialProgress:)mutating 方法,说明更新发生在这个值本身。
  • @State 告诉 SwiftUI 管理这份存储;view struct 重新创建时,SwiftUI 会重新连接到已有状态。
  • presentEditor() 只请求 EditorConfig 更新状态,界面刷新由 SwiftUI 根据状态变化完成。

@Binding 把同一份状态交给子视图修改

07:22

ProgressEditor 需要修改 BookViewEditorConfig。如果直接传值,子视图只会改到副本。如果子视图再声明自己的 @State,就会产生第二份 source of truth。@Binding 的作用是把父视图状态的读写入口传下去。

struct EditorConfig {
    var isEditorPresented = false
    var note = ""
    var progress: Double = 0
}
struct BookView: View {
    @State private var editorConfig = EditorConfig()
    var body: some View {

        ProgressEditor(editorConfig: $editorConfig)

    }
}

struct ProgressEditor: View {
    @Binding var editorConfig: EditorConfig

        TextEditor($editorConfig.note)

}

关键点:

  • $editorConfig 来自 @State 的 projected value,类型是 Binding<EditorConfig>
  • ProgressEditor 接收 @Binding 后,修改的是 BookView 拥有的同一份状态。
  • TextEditor 自身也接收 binding,$editorConfig.note 是从已有 binding 派生出的字符串 binding。
  • Binding 与具体来源无关;它可以来自 @State,也可以来自后面的模型对象或存储 wrapper。

ObservableObject 把模型变化暴露给视图

13:15

当数据需要持久化、同步或处理副作用时,演讲建议进入模型层。ObservableObject 是一个 class-constrained protocol,它通过 objectWillChange publisher 告诉 SwiftUI 对象即将变化。常见写法是在会变化的属性上标 @Published

/// The current reading progress for a specific book.
class CurrentlyReading: ObservableObject {
    let book: Book
    @Published var progress: ReadingProgress

    // …
}

struct ReadingProgress {
    struct Entry : Identifiable {
        let id: UUID
        let progress: Double
        let time: Date
        let note: String?
    }

    var entries: [Entry]
}

关键点:

  • CurrentlyReading 是引用类型,用来管理某一本书的阅读状态。
  • booklet,因为演讲里这本书本身不会变化。
  • progress@Published,所以阅读记录变化前会触发 SwiftUI 的依赖更新。
  • ReadingProgress 仍然是值类型;演讲明确说可以用值类型建模数据,用引用类型管理生命周期和副作用。

@ObservedObject 引用外部模型,@StateObject 拥有模型生命周期

15:36

@ObservedObject 适合读取外部传进来的 ObservableObject。它会让 SwiftUI 订阅对象的 objectWillChange,但不负责创建和保活这个实例。

struct BookView: View {
    @ObservedObject var currentlyReading: CurrentlyReading

    var body: some View {
        VStack {
            BookCard(
                currentlyReading: currentlyReading)

            //…

            ProgressDetailsList(
                progress: currentlyReading.progress)
        }
    }
}

关键点:

  • currentlyReading 是这个视图的 source of truth,但实例生命周期由外部管理。
  • BookCardProgressDetailsList 都从同一对象读取数据,变化时一起刷新。
  • 视图只声明数据如何映射到界面,不需要手写订阅和刷新代码。

20:20

@StateObject 是 2020 年新增的工具。它适合让视图拥有一个 ObservableObject。演讲用封面加载器举例:封面只在视图可见时需要,加载器应该跟随视图生命周期创建和释放。

struct BookCoverView: View {
    @StateObject var loader = CoverImageLoader()

    var coverName: String
    var size: CGFloat

    var body: some View {
        CoverImage(loader.image, size: size)
            .onAppear { loader.load(coverName) }
    }
}

关键点:

  • @StateObject 提供初始值,SwiftUI 会在第一次运行 body 前实例化它。
  • SwiftUI 会在整个视图生命周期内保留 loader,避免 view struct 重建时丢失对象。
  • onAppear 只触发加载动作;对象释放时,CoverImageLoader 自己可以在 deinit 里取消工作。

26:39

演讲专门对比了一个错误写法:在 @ObservedObject 属性上直接创建 ReadingListStore()。因为 view struct 没有稳定生命周期,这会在 body 重建时反复分配对象,造成慢更新和数据丢失。修复方式是改用 @StateObject

struct ReadingListViewer: View {
    var body: some View {
        NavigationView {
            ReadingList()
            Placeholder()
        }
    }
}

struct ReadingList: View {
    @StateObject var store = ReadingListStore()

    var body: some View {
        // ...
    }
}

关键点:

  • ReadingListStoreReadingList 拥有的 source of truth。
  • @StateObject 让 SwiftUI 在正确时间创建对象,并在视图生命周期内保留它。
  • 这个改法同时解决重复堆分配和对象重置两个问题。

@SceneStorage@AppStorage 保存轻量 UI 状态

32:43

@State@StateObject 的生命周期跟进程相关。App 被系统结束后,它们不会自动恢复。演讲引入 Storage 家族,用来保存轻量状态。@SceneStorage 适合每个 scene 独立保存,例如分栏界面当前选中的书。

struct ReadingListViewer: View {
    @SceneStorage("selection") var selection: String?

    var body: some View {
        NavigationView {
            ReadingList(selection: $selection)
            BookDetailPlaceholder()
        }
    }
}

关键点:

  • "selection" 是存储 key,同一类数据需要稳定且唯一的 key。
  • selection@State 一样使用,也能通过 $selection 生成 binding。
  • 每个 scene 可以有自己的选择状态,适合 iPad 或 Mac 上的多窗口。

33:49

@AppStorage 使用 UserDefaults 保存 App 级别的小数据。演讲例子是两个设置开关:是否更新封面图、是否同步阅读进度。

struct BookClubSettings: View {
    @AppStorage("updateArtwork") private var updateArtwork = true
    @AppStorage("syncProgress") private var syncProgress = true

    var body: some View {
        Form {
            Toggle(isOn: $updateArtwork) {
                //...
            }

            Toggle(isOn: $syncProgress) {
                //...
            }
        }
    }
}

关键点:

  • 每个设置使用自己的 key,因为它们是两份独立数据。
  • @AppStorage 默认使用 standard user defaults。
  • Toggle 通过 binding 修改设置值,SwiftUI 自动保存并恢复。
  • 演讲提醒不要把所有数据都塞进 storage wrapper,因为持久化有成本。

核心启发

  1. 阅读进度编辑器做什么:给阅读、课程或健身 App 做一个带笔记的进度更新 sheet。为什么值得做:本 session 的 EditorConfig 展示了如何把多个相关字段收进一个 @State 值。怎么开始:先定义一个值类型配置对象,再把 $editorConfig 传给编辑子视图。

  2. 封面或头像加载视图做什么:把网络图片加载器封装成一个独立 SwiftUI view。为什么值得做@StateObject 能让加载器跟随视图生命周期保活,减少重复分配和状态重置。怎么开始:让 loader 遵守 ObservableObject,在视图里用 @StateObject 创建,在 onAppear 触发加载。

  3. 多窗口选择状态恢复做什么:在 iPad 或 Mac 的列表详情界面里恢复每个窗口的当前选择。为什么值得做@SceneStorage 是 per-scene 存储,适合保存轻量 UI 状态。怎么开始:为选择值定义唯一 key,把 $selection 传给列表视图。

  4. 轻量设置页做什么:实现是否同步、是否更新封面、是否显示某个区域这类开关。为什么值得做@AppStorage 直接连接 UserDefaults,适合 App 级小设置。怎么开始:给每个设置声明一个 @AppStorage 属性,再把 binding 传给 Toggle

  5. 全局数据入口做什么:把书单、账户状态或当前工作区模型放在 SwiftUI App 根部。为什么值得做:演讲展示了在 @main App 里用 @StateObject 创建 app-wide source of truth,所有 scene 都能响应模型变化。怎么开始:在 App 类型里声明 @StateObject private var store,再把 store 传给根视图或注入环境。


关联 Session

评论

GitHub Issues · utterances