SwiftUI协议ObservableObject
SwiftUI协议ObservableObject

SwiftUI协议ObservableObject

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,并重新读取所有当前属性的值。

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注