ObservableObject 是一个Swift 协议(位于 Combine 框架中),用于把一个类标记为“可被观察的对象”。
遵循该协议的实例可以向订阅者(通常是SwiftUI)发布即将发生的改变,从而触发View视图更新或其他想要逻辑。通常用于MVVM中的ViewModel。
import Combine
protocol ObservableObject: AnyObject {
var objectWillChange: ObservableObjectPublisher { get }
}
ObservableObject实现了ObservableObjectPublisher(Combine 的 class ObservableObjectPublisher: Publisher),并在SwiftUI中被用于订阅变化。
基本用法
ObservableObject通常配合 @Published 属性包装器使用,被@Published标记的属性在发生改变前,会自动向objectWillChange发布事件(即自动调用 objectWillChange.send()),所以通常不需手动发送通知。
例如:
class Counter: ObservableObject {
@Published var value = 0
}
当value改变时,objectWillChange 会自动发出事件,SwiftUI订阅并自动刷新使用它的View视图。
SwiftUI订阅
SwiftUI在使用@StateObject、@ObservedObject、@EnvironmentObject 等包装器时,会自动订阅 objectWillChange。当发布者发送事件,SwiftUI 会在主线程的下一个更新周期重新计算依赖该对象的视图 body。
例如:
struct ContentView: View {
@StateObject private var vm = MyViewModel()
var body: some View {
Text(vm.title)
}
}
当vm发生变化(通过@Published或objectWillChange.send()),SwiftUI会刷新body视图。
手动更新View
如果属性不是通过 @Published修饰,而是其他的修饰符(@AppStorage)。在非View视图中,属性发生变化时,不会触发View视图刷新。这时就需要手动调用objectWillChange.send()方法。
@AppStorage("ts_is_new_user")
var isNewUser: Bool = false {
willSet { objectWillChange.send() }
}
objectWillChange 是 ObservableObject 的发布者实例。可以手动调用 objectWillChange.send() 来通知观察者对象即将发生变化。
适用于以下几个场景:
1、属性不是 @Published(例如复杂/复合值或引用类型内部变化)。
2、需要在改变前批量通知(例如一次改变多个字段,但只想触发一次刷新)。
3、使用自定义发布逻辑或要保证在某时间点通知。
final class UserStore: ObservableObject {
// 不是 @Published
var profile: Profile
func updateProfile(_ newProfile: Profile) {
objectWillChange.send()
profile = newProfile
}
}
注意:无意义的调用会造成重绘或不必要的开销。
objectWillChange.send()可以在任意线程调用,但是SwiftUI需要在主线程更新UI。如果从后台线程中调用send()方法,SwiftUI会在主线程更新时刷新,因此最好在主线程中调用send()方法,以避免竟态或不可预测的行为。
DispatchQueue.main.async {
self.objectWillChange.send()
}
注意事项
1、值类型(struct)与引用类型(class)的差异
使用 @Published 的属性若是值类型(struct),直接赋新值会触发发布。修改 struct 的内部属性必须是通过赋新值来使发布器感知(因为 struct 是值语义)。
当属性是引用类型(class),改变其内部状态不会自动触发 @Published(除非把引用替换为新实例或该引用自身也发布变化)。因此对引用类型内部变化通常需要手动 objectWillChange.send() 或使引用类型也遵循 ObservableObject 并订阅其发布。
class InnerRef {
var count = 0
}
final class VM: ObservableObject {
// 自动触发
@Published var title: String = ""
// 引用类型,内部变化不会自动触发
var ref = InnerRef()
func incrementRef() {
objectWillChange.send() // 手动通知一次
ref.count += 1
}
}
2、重复刷新:避免在 willSet/didSet、@Published、和手动 send() 多重触发。重复调用会导致多次渲染。
3、滥用 willSet { objectWillChange.send() }:如果属性已经是 @Published 或被 View 直接使用,通常不需要额外 willSet。
4、线程问题:最好在主线程发送通知并更新 UI 相关状态。
5、性能:避免微调 UI 更新频率的 premature optimization,但如果遇到性能问题,考虑合并多次更改为单次 send()。
6、测试:在单元测试里可以订阅 objectWillChange 来断言发送事件发生。
总结
在MVVM模式中,ViewModel持有变量、方法供多个View共享时,通常需要使用ObservableObject管理。
ObservableObject实例跨View共享、可被观察并触发UI更新。
扩展知识
1、和Combine的关系
protocol ObservableObject: AnyObject {
var objectWillChange: ObservableObjectPublisher { get }
}
ObservableObjectPublisher 遵循 Publisher 协议,输出类型为空元组 Void(或 Never 为 Failure)。
SwiftUI 使用 AnyCancellable 去订阅这个 publisher。
@Published 的实现会在 setter 内部调用 objectWillChange.send(),并将发布的具体属性值通过 Publisher 暴露出来($property)。
2、objectWillChange.send()语义
objectWillChange.send()表示向订阅者发送“对象即将发生变化”的信号,会使所有 订阅这个 ObservableObject 的 View 重新运行 body。
订阅者(通常是SwiftUI视图)收到这个信号后,会在下一个刷新周期重新计算以来这个对象的View视图。
注意:不是所有View都会刷新,SwiftUI是按对象粒度来刷新的,而不是字段粒度。
SwiftUI内部订阅objectWillChange,当对象调用:
objectWillChange.send()
SwiftUI会知道对象即将发生变化,需要重新渲染关联该对象的View视图。
例如:
struct AView: View {
@ObservedObject var store: UserStore
var body: some View {
Text(store.profile.name)
}
}
当调用:
store.objectWillChange.send()
store.profile = ...
SwiftUI监测到Text中涉及store的字段,重新运行body(),让UI读取最新的属性之。
不涉及UserStore对象的视图不会刷新。
send() 执行顺序:
final class UserStore: ObservableObject {
// 不是 @Published
var profile: Profile
func updateProfile(_ newProfile: Profile) {
objectWillChange.send()
profile = newProfile
}
}
在调用updateProfile方法时,这里执行顺序可能存在歧义,实际上send()和修改变量的顺序并不会影响代码。
因为调用send()方法时,它会标记视图需要更新,等到下一次主线程刷新周期(下一帧)重新运行body,并重新读取所有当前属性的值。
