SwiftUI Notes


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 之间的联系. 但是, 只要这个ObservableObjectobjectWillChange发出信息, 则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和旧的完全无关.

StateObject (iOS 14+)

一个 property wrapper, 综合了StateObservedObject的特点:

  • 包含的对象是自定义的遵循了ObservableObject的类型.
  • 对象的生命期和 runtime view 保持一致.

待确认的点:

  • 是否需要 getter 在body中被调用的前提, 才能在objectWillChange产生信息时触发body. 即是类似ObservedObject, 还是类似State?

语法特点

  • StateObject wrapper 的初始化方法是一个@autoclosure, 这意味着和StateObservedObject均不同: 只有当对应 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: UIViewControllerRepresentableUIViewRepresentable.
    • 两者都遵循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 方法.

UI Tips

SwiftUI 毕竟和之前熟悉的 UIKit 是2套 API. 这里记录一些琐碎的 UI 相关实现 tips.

  • 实现 navigation: 把NavigationLink放到NavigationView.
  • 实现 presentation: .sheet().
  • 不通过 click/tap gesture 实现navigation的 callback, 可以通过isActive参数, 传入一个自定义的ObservableObject对象, 就可以实现对子页面是否显示的监听.

待确认的点

  • SceneViewonChange方法, 传入 value 是怎么 work 的.

  1. 本文所有单独出现的 view, 都指真正的 runtime 对象, 和Viewview struct 含义有区别, 后者仅仅是 recipe/description. ↩︎