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
添加懒加载机制,使得这种问题能够更加自然地解决。
"加入我们的 Discord 社区,与超过 2000 名苹果生态的中文开发者一起交流!"