Observation 框架为 Swift 带来了原生的属性级观察能力,有效避免了 SwiftUI 中因无关属性变化而引发的多余视图更新,从而提升了应用性能。但由于 @State 并未提供类似 @StateObject 的懒加载构造方式,在某些场景下会因实例过早构建而引起性能损失甚至逻辑问题。本文将探讨如何为 Observable 实例定制一个支持懒加载的 @State 解决方案。
问题示例
在 SwiftUI 中,视图实例的创建与加载到视图树中并非一一对应。在许多情况下,视图实例可能会被提前或多次创建。例如,下面的代码中,即便你尚未进入导航容器的下一层(LinkViewUsingObservation),SwiftUI 仍会提前构建该视图中的可观察实例 TestObject。
import Observation
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink {
LinkViewUsingObservation()
} label: {
Text("Hello")
}
}
}
}
struct LinkViewUsingObservation: View {
@State var object = TestObject()
var body: some View {
Text("State Observation")
}
}
@Observable
class TestObject {
init() {
print("init")
}
}如你所见,NavigationLink 会提前创建 LinkViewUsingObservation 的实例。设想在使用 List 展示大量 LinkViewUsingObservation 时,这种提前构建将不可避免地带来性能损失。
若将实现改为基于 ObservableObject,则提前构建实例的问题便不会出现,因为 TestObject 只会在导航进入 LinkViewUsingStateObject 视图后才被构造:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink {
LinkViewUsingStateObject()
} label: {
Text("Hello")
}
}
}
}
struct LinkViewUsingStateObject: View {
@StateObject var object = TestObject()
var body: some View {
Text("StateObject")
}
}
class TestObject: ObservableObject {
init() {
print("init")
}
}StateObject 的懒加载机制
StateObject 之所以不会在视图实例创建时立即构建 TestObject,是因为它采用了懒加载策略。其构造方法如下所示:
@inlinable nonisolated public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)在视图真正加载时,StateObject 才会调用 thunk 闭包来创建并持有 ObservableObject 实例,从而避免了不必要的提前构建。你可以在 《避免 SwiftUI 视图的重复计算》 一文中找到对其懒加载实现的详细解析。
然而,当将原有的 ObservableObject 实现替换为 Observable 时,由于 @State 并未提供类似的懒加载机制,开发者便无法享受延迟构造的优势。
有效但不优雅的解决方案
一种较简单的替代方案是,让 Observable 实例同时遵循 ObservableObject 协议,并继续使用 @StateObject 声明。这样既能保持懒加载特性,又可在视图中响应属性变化,从而避免无效更新:
struct LinkViewUsingStateObject: View {
@StateObject var object = TestObject() // 使用 StateObject 来声明
var body: some View {
let _ = print("update")
Text("StateObject")
Text(object.name)
Button("Change Name"){
object.name = "\(Int.random(in: 0...1000))"
}
Button("Change Age"){
object.age = Int.random(in: 0...100)
}
}
}
@Observable
class TestObject: ObservableObject { // 增加 ObservableObject
init() {
print("init")
}
var name = "abc"
var age = 10
}不过,这种方式容易引发混淆——在团队协作中,成员可能难以区分到底采用了哪种观察机制。
@LazyState:支持懒加载的 @State 实现
针对上述问题,已有不少开发者向苹果反馈,期望未来能为 @State 添加懒加载机制。在苹果未作出修改之前,我们可以通过自定义属性包装器来实现这一功能:
@MainActor // 确保属性包装器在主线程操作,保证在调用 wrappedValue 前完成 setup
@propertyWrapper
public struct LazyState<T: Observable>: @preconcurrency DynamicProperty { // 限定使用在 Observable 类型上
@State private var holder: Holder
// 保持与 State 和 StateObject 的一致性,实例只能创建一次,不可修改( 不创建 setter )
public var wrappedValue: T {
holder.wrappedValue
}
public var projectedValue: Binding<T> {
// 只需要通过 keyPath 修改数据,因此忽略 setter,
return Binding(get: { wrappedValue }, set: { _ in })
}
// 当视图加载时调用,创建实例
public func update() {
guard !holder.onAppear else { return }
holder.setup()
}
public init(wrappedValue thunk: @autoclosure @escaping () -> T) {
_holder = State(wrappedValue: Holder(wrappedValue: thunk()))
}
}
extension LazyState {
// 用于持有实例的助手类
final class Holder {
private var object: T!
private let thunk: () -> T
// 标记实例是否已初始化,避免重复创建
var onAppear = false
var wrappedValue: T {
object
}
func setup() {
object = thunk() // 延迟初始化实例
onAppear = true // 标记为已初始化,防止重复调用
}
init(wrappedValue thunk: @autoclosure @escaping () -> T) {
self.thunk = thunk
}
}
}现在你便可以使用 @LazyState 来声明 Observable 实例。待苹果对 @State 进行增强后,我们只需简单地切换回来即可:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink {
LinkViewUsingLazyState()
} label: {
Text("Hello")
}
}
}
}
struct LinkViewUsingLazyState: View {
@LazyState var object = TestObject()
var body: some View {
Text("LazyState")
}
}
@Observable
class TestObject {
init() {
print("init")
}
}总结
Observation 框架极大地提升了 SwiftUI 的性能,但由于其实现机制的变化,开发者仍需根据项目特点做出相应调整。期待苹果能尽快为 @State 添加懒加载机制,使得这种问题能够更加自然地解决。