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 需要修改 BookView 的 EditorConfig。如果直接传值,子视图只会改到副本。如果子视图再声明自己的 @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是引用类型,用来管理某一本书的阅读状态。book用let,因为演讲里这本书本身不会变化。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,但实例生命周期由外部管理。BookCard和ProgressDetailsList都从同一对象读取数据,变化时一起刷新。- 视图只声明数据如何映射到界面,不需要手写订阅和刷新代码。
(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 {
// ...
}
}
关键点:
ReadingListStore是ReadingList拥有的 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,因为持久化有成本。
核心启发
-
阅读进度编辑器:做什么:给阅读、课程或健身 App 做一个带笔记的进度更新 sheet。为什么值得做:本 session 的
EditorConfig展示了如何把多个相关字段收进一个@State值。怎么开始:先定义一个值类型配置对象,再把$editorConfig传给编辑子视图。 -
封面或头像加载视图:做什么:把网络图片加载器封装成一个独立 SwiftUI view。为什么值得做:
@StateObject能让加载器跟随视图生命周期保活,减少重复分配和状态重置。怎么开始:让 loader 遵守ObservableObject,在视图里用@StateObject创建,在onAppear触发加载。 -
多窗口选择状态恢复:做什么:在 iPad 或 Mac 的列表详情界面里恢复每个窗口的当前选择。为什么值得做:
@SceneStorage是 per-scene 存储,适合保存轻量 UI 状态。怎么开始:为选择值定义唯一 key,把$selection传给列表视图。 -
轻量设置页:做什么:实现是否同步、是否更新封面、是否显示某个区域这类开关。为什么值得做:
@AppStorage直接连接UserDefaults,适合 App 级小设置。怎么开始:给每个设置声明一个@AppStorage属性,再把 binding 传给Toggle。 -
全局数据入口:做什么:把书单、账户状态或当前工作区模型放在 SwiftUI App 根部。为什么值得做:演讲展示了在
@main App里用@StateObject创建 app-wide source of truth,所有 scene 都能响应模型变化。怎么开始:在 App 类型里声明@StateObject private var store,再把 store 传给根视图或注入环境。
关联 Session
- App essentials in SwiftUI — 继续讲 SwiftUI App、Scene、WindowGroup 和 App 级 source of truth。
- What’s new in SwiftUI — 总览 2020 年 SwiftUI 更新,包括 App/Scene API、控件、网格和 widget 支持。
- Build document-based apps in SwiftUI — 展示
DocumentGroup如何把文档数据、场景和 SwiftUI 绑定起来。 - Stacks, Grids, and Outlines in SwiftUI — 关注 SwiftUI 中详细数据和层级数据的展示方式。
评论
GitHub Issues · utterances