Data Flow
State
Property wrapper State
只是一个映射, 并不真正开放空间存储变量, 真正的空间开辟是这样的过程:
State
必须定义在某个ViewA: View
中作为成员属性, 不能定义在别的地方, 也不能定义成optional, 也不能定义成static变量.- SwiftUI 在
ViewA
对象生成时, 通过某种和DynamicProperty
相关的机制(可能是某种 reflection 机制, 也可能是 compiler synthesize), 根据State
中指示的 initial value, 在真正的 view[1] 对象中建立了一份数据, 并把ViewA
和其所有State
的成员变量作出了 Model -> View 的关联. - 之后
ViewA.body
每次被重新调用 (例如 data change 导致的 update 或者 view 重新出现在屏幕上等) 前, 都会保证ViewA
中可以访问到的值被更新到与内存中的运行时状态一致, 这样之后访问/操纵该值时, 本质上都是访问了对应的真正的与 view 关联的数据对象. - 在 SwiftUI 可以监控到的环境下调用到某个
State
的 getter 的前提下, 我们只要在任何地方触发这个State
的 setter, 就有可能触发获取了 getter 的body
的重新调用.- 当
State
wrap 的类型是非Equatable
时, 只要触发 setter, 即触发body
被调用. - 当
State
wrap 的类型遵循Equatable
时, 只有触发 setter 时的newValue
和旧值不同时, 才会触发body
.
- 当
注意在非 SwiftUI 体系下, State
毫无作用, 甚至基本的 get/set 逻辑都不满足.
State
所指示对象的生命期
无论是State
, 还是我们自定义的struct ViewA
, 本质上都只是 recipe, 不是真正的运行时对象. 真正的运行时对象, 要么被 SwiftUI 很好地隐藏在真正的 runtime view 对象中, 要么在我们自定义的别的 reference type 中(类似下文的ObservableObject).
State
指示的真正的对象的生命期和对应的 view 一致, 无论该 view 重绘几次, 只要没有从内存中清除, 都只初始化一次.- 如果 parent view 刷新导致重新初始化了一个 child view 对应的 struct, 只要该 child view 在整个 view hierarchy 中的位置没有变化, SwiftUI 会很聪明地复用之前的 runtime 信息, 不会重新初始化
State
对应的数据对象. - 如果这个 view 销毁再重新创建, 那么 SwiftUI 会用初始化值重新给新的 view 开辟一份.
State
wrapper 的语法特征
- 因为
State
的 setter 是nonmutating
的, 因此:- 即使我们在
body
中 call 了 setter, 也无需把这个 struct 的body
成员标记成mutating
, 或者无需显示 capture 一个var self
. - 如果不用 property wrapper 的写法, 可以直接声明成
let xxx: State // (无需 var)
, 需要引用时, 使用使用wrappedValue
的 setter/getter.
- 即使我们在
- 因为
State
其实只是一个 wrapper, 不存储实际的 runtime value, 所以我们在 callback 中修改 struct 中的State
对象时, 虽然从语法角度 block 必然会 capture 一份self
, 导致修改的只是State
副本, 但在 SwiftUI 系统中是 work 的.
ObservableObject
- 一个 protocol, 供我们实现. 默认情况下, 如果变量有
@Published
, 那么 compiler 会自动帮我们实现需求的方法. - 如果手动实现方法:
- 则必须实现一个
Publisher
类型的成员objectWillChange
, 其Failure == Never
. 一般可以直接用ObservableObjectPublisher
这个 concrete type. - SwiftUI 会在合适的时候给这个 publisher 增加 subscriber.
- 要求我们的 code 在合适的时机(必须符合 “will” 的语义)通过
objectWillChange
发出信息.
- 则必须实现一个
- SwiftUI中的
@ObservedObject
可以将ObservableObject
用 property wrapper 包装. . - SwiftUI中的
@EnvironmentObject
同样可以包装ObservableObject
.
ObservedObject
一个 property wrapper, 包装遵循ObservableObject
的对象. 类似State
使用, 但和State
有区别.
- 设定成
View
的成员变量后, 和State
类似, SwiftUI 会建立 model 和 view 之间的联系. 但是, 只要这个ObservableObject
的objectWillChange
发出信息, 则body
就会被重新调用, 不需要在 SwiftUI 可以监控到的环境下首先被调用过 getter. - 这个
ObservedObject
成员对象的内存生命期不和 runtime view 一一绑定, 而是由我们的 code 控制- 如果这个 property 使用 dependency injection 由外部传入, 那么只要外部的引用不消亡, 则始终是同一份对象.
- 如果这个 property 是在当前 struct 中初始化, 那么该对象的生命期和当前 struct 被初始化和 pass-by-value 的范围有关.
- 如果只是当前 view 被重绘, 触发
body
重新调用, 那么这个对象仍然存活. - 如果是 parent view 被重绘, 触发初始化一个新的 child view struct, 那么旧的
View
对象所有引用计数终会清空, 而新的 view 用到的ObservedObject
和旧的完全无关.
- 如果只是当前 view 被重绘, 触发
StateObject (iOS 14+)
一个 property wrapper, 综合了State
和ObservedObject
的特点:
- 包含的对象是自定义的遵循了
ObservableObject
的类型. - 对象的生命期和 runtime view 保持一致.
待确认的点:
- 是否需要 getter 在
body
中被调用的前提, 才能在objectWillChange
产生信息时触发body
. 即是类似ObservedObject
, 还是类似State
?
语法特点
StateObject
wrapper 的初始化方法是一个@autoclosure
, 这意味着和State
、ObservedObject
均不同: 只有当对应 view 要被渲染时 初始值表达式才会被求值, 而非 view struct 被初始化时.
Binding
- 一个 property wrapper, 包装一个 setter/getter 对.
- 可以从
State
,ObservedObject
等对象通过$
前缀 + keypath 导出. - 一般
State
包裹的泛型类型是一个 struct, 所以只要局部有变化, 整体即产生变化. 因此, 即使用$
运算生成部分 key path 的Binding
, 只要部分值变化, 就会触发所有引用到State
getter 或关联的Binding
的 view 进行重新获取body
.
Framework integration
- 把 SwiftUI 放入 UIKit:
UIHostingController
, 一个UIViewController
的 concrete 子类. - 把 UIKit 放入 SwiftUI:
UIViewControllerRepresentable
和UIViewRepresentable
.- 两者都遵循
View
, 并对所有View
要求的方法进行了默认实现 - 基本要求遵循者
- 提供初始的
UIViewController
/UIView
. - 基于当前 model 进行 view update.
- 提供初始的
- 该 view 初次展现时, 系统会根据
Coordinator
associated type 实现创建并维持一个 coordinator, 在初始化和 update 时作为参数提供给核心 protocol 的实现者. 使实现者可以完成一些 binding. - 最常见的 model 驱动顺序为:
- 创建 protocol 遵循者的实例时, 将 model dependency (例如
Binding
) 传入. - 第一次渲染前, 系统创建 coordinator, 可以把 protocol 遵循者本体 inject 到这个 coordinator 中. 同时使 coordinator 实现 UIKit 中的一些 event pattern.
- 在创建实际的 vc/view 时, 系统会把早前实例的 coordinator 传递给 protocol 的遵循者, 并在这里完成 event 的绑定.
- 当发生 UIKit 体系内的 event 时, 触发 coordinator 中的回调, 进而在 coordinator 中操纵 injected model dependency.
- SwiftUI 触发所有引用到
@State
的 view 的变化, 包括核心 protocol 遵循者的 update 方法.
- 创建 protocol 遵循者的实例时, 将 model dependency (例如
- 两者都遵循
UI Tips
SwiftUI 毕竟和之前熟悉的 UIKit 是2套 API. 这里记录一些琐碎的 UI 相关实现 tips.
- 实现 navigation: 把
NavigationLink
放到NavigationView
. - 实现 presentation:
.sheet()
. - 不通过 click/tap gesture 实现
navigation
的 callback, 可以通过isActive
参数, 传入一个自定义的ObservableObject
对象, 就可以实现对子页面是否显示的监听.
待确认的点
Scene
和View
的onChange
方法, 传入 value 是怎么 work 的.
本文所有单独出现的 view, 都指真正的 runtime 对象, 和
View
或 view struct 含义有区别, 后者仅仅是 recipe/description. ↩︎