Swift 5.9 新 @Observable 对象在 SwiftUI 使用中的陷阱与解决

概览

在 Swift 5.9 中,苹果为我们带来了全新的可观察框架 Observation,它是观察者开发模式在 Swift 中的一个全新实现。

除了自身本领过硬以外,Observation 框架和 SwiftUI 搭配起来也能相得益彰,事倍功半。不过 Observable 对象在 SwiftUI 中干起活来可得特别注意,稍不留神结果就会出乎秃头码农们的意料之外。

在本篇博文中,您将学到如下内容:

  1. 什么是 Observable 对象?
  2. SwiftUI 中对于 Observable 对象承载的两种方式
  3. "原形毕露?"
  4. 溯本回原

闲言少叙,让我们马上开始 Observable 的探险之旅吧!Let's go!!!;)


1. 什么是 Observable 对象?

Observable 对象(不同于之前的 ObservedObject 对象)是 Swift 5.9 新 Observation 框架中推出的一种原生可观察对象。

Observation 框架在 Swift 中提供了观察者设计模式的一个健壮、类型安全和高性能的实现。该模式允许可观察对象维护观察者列表,并通知它们特定属性的改变。这样做的优点是可以实现对象的低耦合,并允许在潜在多个观察者之间隐式分布更新。

创建 Observable 对象很简单,我们只需用 @Observable 宏修饰对应类的定义,该类的实例即为 Observable 对象:

swift 复制代码
@Observable
class Foo {
    var name: String
    var age: Int
    var power: Double
    
    init(name: String, age: Int, power: Double) {
        self.name = name
        self.age = age
        self.power = power
    }
}

// 创建 Observable 对象 foo
let foo = Foo(name: "hopy", age: 11, power: 5)

2. SwiftUI 中对于 Observable 对象承载的两种方式

在 SwiftUI 中,我们可以同样用 @State 属性包装器来对 Observable 对象声明"真相之源":

swift 复制代码
@Observable
class Model {
    var value: Int
    
    init(_ value: Int) {
        self.value = value
    }
}

struct ContentView: View {
	@State var model = Model(11)
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("value: \(model.value)")
                Button("add 1") {
                    model.value += 1
                }
            }
        }
    }
}

大家可以看到,@Observable 对象的行为和之前的 ObservableObject 对象如出一辙,其内容的更改也会导致界面的刷新:

不过,与之前旧 ObservedObject 对象所不同的是,@Observable 对象在 SwiftUI 中无需显式用属性包裹器(Property Wrapper)修饰也能及时的根据变化刷新视图:

swift 复制代码
struct ContentView: View {    
    let model = Model(11)
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("value: \(model.value)")
                
                Button("add 1") {
                    model.value += 1
                }
            }
        }
    }
}

在上面的代码中,我们的 model 对象没有任何修饰,只是一个单纯的 let 属性。不过运行可以发现,它的结果和之前一毛一样!

这难道意味着在 SwiftUI 中用 @State 或 let 来定义 @Observable 对象没有任何区别么?

非也非也!

3. "原形毕露?"

我们知道在 SwiftUI 中视图其实都是状态的函数。但状态不仅仅是视图的简单附庸,它们又可以超然于视图之外。

简单来说:当视图 body 被重新"求值"时,非状态值会被重建,但状态不会!因为状态的生成周期被放在一个单独的存储区内,和视图本身是分开的。

swift 复制代码
struct SubView: View {
    let model = Model(0)
    
    var body: some View {
        RoundedRectangle(cornerRadius: 15.0)
            .fill(.red)
            .frame(width: 150, height: 150)
            .overlay {
                VStack {
                    Text("\(model.value)")
                }
            }.onTapGesture {
                model.value += 1
            }
            .foregroundStyle(.white)
    }
}

struct SubStateView: View {
    @State var model = Model(0)
    
    var body: some View {
        RoundedRectangle(cornerRadius: 15.0)
            .fill(.green)
            .frame(width: 150, height: 150)
            .foregroundStyle(.white)
            .overlay {
                VStack {
                    Text("\(model.value)")
                }
            }.onTapGesture {
                model.value += 1
            }
            .foregroundStyle(.white)
    }
}

struct ContentView: View {
    
   @State var id = Int.random(in: 0...10000)
    
   var body: some View {
        NavigationStack {
            VStack {
                GroupBox("无状态 @Observable 对象") {
                    SubView()
                }
                
                GroupBox("@State @Observable 对象") {
                    SubStateView()
                }
                
                Button("刷新:\(id)") {
                    id = Int.random(in: 0...10000)
                }
            }
            .padding()
            .font(.largeTitle.weight(.black))
            .navigationTitle("@Observable 对象演示")
        }
    }
}

对于上面的代码来说,我们在主视图中创建了两个子视图:SubView 和 SubStateView。其中 @Observable 对象 model 在前者中不以状态承载,而在后者中作为状态承载。

运行结果如上图所示:当用户点击刷新按钮时会引起主视图中 Text 显示内容的改变,从而导致主视图中两个子视图发生重建。可以看到以状态承载的 @Observable 对象保持稳定,而另一个 @Observable 对象被重建了。

4. 溯本回原

从上面我们知道了问题的症结,所以改善起来就很简单了:我们只需要保持 @Observable 对象本身的稳定性即可。

一种办法是在主视图中以状态承载该对象,然后将其传递到子视图中去:

swift 复制代码
struct SubView: View {
    let model: Model
    
    var body: some View {
        RoundedRectangle(cornerRadius: 15.0)
            .fill(.red)
            .frame(width: 150, height: 150)
            .overlay {
                VStack {
                    Text("\(model.value)")
                }
            }.onTapGesture {
                model.value += 1
            }
            .foregroundStyle(.white)
    }
}

struct ContentView: View {
    @State var model = Model(0)
    @State var id = Int.random(in: 0...10000)
    
   var body: some View {
        NavigationStack {
            VStack {
                SubView(model: model)
                
                Button("刷新:\(id)") {
                    id = Int.random(in: 0...10000)
                }
            }
            .padding()
            .font(.largeTitle.weight(.black))
            .navigationTitle("@Observable 对象演示")
            .toolbar {
                Text("大熊猫侯佩 @ CSDN")
                    .font(.headline.weight(.bold))
                    .foregroundStyle(.gray)
            }
        }
    }
}

在上面的代码中,如果可以保证主视图(ContentView)本身不被重建,那么使用非状态来承载 model 对象也是可以的(但不推荐):

swift 复制代码
struct ContentView: View {
    let model = Model(0)
	
	var body: some View {
        NavigationStack {
            VStack {
            	// 由于主视图的强稳定性,所以 SubView 对于 model 的引用也保持强稳定(即使是非状态)
                SubView(model: model)
                
                Button("刷新:\(id)") {
                    id = Int.random(in: 0...10000)
                }
            }
        }
    }
}

当然,如果需要在子视图中也能更改 @Observable 对象本身,我们可以直接使用 @Bindable 来修饰它们。

现在,小伙伴们今后倘若在 SwiftUI 遇到类似的问题,相信也可以迎刃而解啦,棒棒哒!💯


更多 Swift 5.9 中新 Observation 框架知识的介绍,请小伙伴们移步如下链接观赏:


总结

在本篇博文中,我们讨论了在 SwiftUI 中融合 Swift 5.9 新 @Observable 对象的几种方式,并比较了它们细微差别下的潜在陷阱,最后提供了非常简单的解决之道。

感谢观赏,再会!8-)

相关推荐
Mirageef2 小时前
aardio 并行任务处理
编程语言
大熊猫侯佩6 小时前
SwiftUI 更自然地向自定义视图传递参数的“另类”方式
swiftui·swift·apple
大熊猫侯佩6 小时前
UIKit 在 UICollectionView 中拖放交换 Cell 视图的极简实现
swift·apple·ui kit
Moonbit21 小时前
MoonBit 地区大使持续招募中:语言走向稳定,社区加速壮大!
编程语言
杂雾无尘1 天前
iOS 分享扩展(五):解锁 iOS 分享面板的神秘的联系人推荐功能
ios·swift·客户端
用户0595661192091 天前
Java 21 与 Spring Boot 3.2 微服务开发从入门到精通实操指南
java·spring·编程语言
大熊猫侯佩1 天前
SwiftUI 集合视图(Grid)拖放交换 Cell 的极简实现
swiftui·swift·apple