在 SwiftUI 中,如何对 "View" 的“@Published vars”*外部*的变化做出反应

In SwiftUI, how to react to changes on "@Published vars" *outside* of a "View"

假设我有以下 ObservableObject,它每秒生成一个随机字符串:

import SwiftUI

class SomeObservable: ObservableObject {

    @Published var information: String = ""

    init() {
        Timer.scheduledTimer(
            timeInterval: 1.0,
            target: self,
            selector: #selector(updateInformation),
            userInfo: nil,
            repeats: true
        ).fire()
    }

    @objc func updateInformation() {
        information = String("RANDOM_INFO".shuffled().prefix(5))
    }
}

还有一个 View,它观察到:

struct SomeView: View {

    @ObservedObject var observable: SomeObservable

    var body: some View {
        Text(observable.information)
    }
}

以上将按预期工作。
ViewObservableObject 更改时重绘自身:

现在开始提问:

我怎么能在 "pure" struct 中做同样的事情(比如调用一个函数),它也观察到相同的 ObservableObject? "pure" 我的意思是 符合 View:

struct SomeStruct {

    @ObservedObject var observable: SomeObservable

    // How to call this function when "observable" changes?
    func doSomethingWhenObservableChanges() {
        print("Triggered!")
    }
}

(它也可以是 class,只要它能够对可观察对象的变化做出反应即可。)

从概念上讲似乎很容易,但我显然遗漏了一些东西。

(注意:我使用的是 Xcode 11,beta 6。)


更新(供未来的读者使用)(粘贴到 Playground)

这是一个可能的解决方案,基于@Fabian 提供的出色答案:

import SwiftUI
import Combine
import PlaygroundSupport

class SomeObservable: ObservableObject {

    @Published var information: String = "" // Will be automagically consumed by `Views`.

    let updatePublisher = PassthroughSubject<Void, Never>() // Can be consumed by other classes / objects.

    // Added here only to test the whole thing.
    var someObserverClass: SomeObserverClass?

    init() {
        // Randomly change the information each second.
        Timer.scheduledTimer(
            timeInterval: 1.0,
            target: self,
            selector: #selector(updateInformation),
            userInfo: nil,
            repeats: true
        ).fire()    }

    @objc func updateInformation() {
        // For testing purposes only.
        if someObserverClass == nil { someObserverClass = SomeObserverClass(observable: self) }

        // `Views` will detect this right away.
        information = String("RANDOM_INFO".shuffled().prefix(5))

        // "Manually" sending updates, so other classes / objects can be notified.
        updatePublisher.send()
    }
}

class SomeObserverClass {

    @ObservedObject var observable: SomeObservable

    // More on AnyCancellable on: apple-reference-documentation://hs-NDfw7su
    var cancellable: AnyCancellable?

    init(observable: SomeObservable) {
        self.observable = observable

        // `sink`: Attaches a subscriber with closure-based behavior.
        cancellable = observable.updatePublisher
            .print() // Prints all publishing events.
            .sink(receiveValue: { [weak self] _ in
            guard let self = self else { return }
            self.doSomethingWhenObservableChanges()
        })
    }

    func doSomethingWhenObservableChanges() {
        print(observable.information)
    }
}

let observable = SomeObservable()

struct SomeObserverView: View {
    @ObservedObject var observable: SomeObservable
    var body: some View {
        Text(observable.information)
    }
}
PlaygroundPage.current.setLiveView(SomeObserverView(observable: observable))

Result

(注意:必须 运行 应用才能检查控制台输出。)

旧方法是使用您注册的回调。较新的方法是使用 Combine 框架创建发布者,您可以为其注册进一步的处理,或者在本例中是 sink,每次 source publisher 发送消息时都会调用它。此处的发布者不发送任何内容,因此类型为 <Void, Never>.

计时器发布者

要从计时器获取发布者,可以直接通过 Combine 或通过 PassthroughSubject<Void, Never>() 创建通用发布者,注册消息并通过 [=] 在 timer-callback 中发送它们18=]。该示例有两种变体。

ObjectWillChange 发布者

每个 ObservableObject 都有一个 .objectWillChange 发布者,您可以像为 Timer publishers 一样注册 sink。每次调用它或每次 @Published 变量更改时都应该调用它。但是请注意,这是在更改之前调用的,而不是在更改之后调用的。 (DispatchQueue.main.async{} 改变完成后在sink里面做出反应)。

正在注册

每个接收器调用都会创建一个必须存储的 AnyCancellable,通常存储在与 sink 应该具有相同生命周期的对象中。一旦可取消的被解构(或调用它的 .cancel()),sink 就不会被再次调用。

import SwiftUI
import Combine

struct ReceiveOutsideView: View {
    #if swift(>=5.3)
        @StateObject var observable: SomeObservable = SomeObservable()
    #else
        @ObservedObject var observable: SomeObservable = SomeObservable()
    #endif

    var body: some View {
        Text(observable.information)
            .onReceive(observable.publisher) {
                print("Updated from Timer.publish")
        }
        .onReceive(observable.updatePublisher) {
            print("Updated from updateInformation()")
        }
    }
}

class SomeObservable: ObservableObject {
    
    @Published var information: String = ""
    
    var publisher: AnyPublisher<Void, Never>! = nil
    
    init() {
        
        publisher = Timer.publish(every: 1.0, on: RunLoop.main, in: .common).autoconnect().map{_ in
            print("Updating information")
            //self.information = String("RANDOM_INFO".shuffled().prefix(5))
        }.eraseToAnyPublisher()
        
        Timer.scheduledTimer(
            timeInterval: 1.0,
            target: self,
            selector: #selector(updateInformation),
            userInfo: nil,
            repeats: true
        ).fire()
    }
    
    let updatePublisher = PassthroughSubject<Void, Never>()
    
    @objc func updateInformation() {
        information = String("RANDOM_INFO".shuffled().prefix(5))
        updatePublisher.send()
    }
}

class SomeClass {
    
    @ObservedObject var observable: SomeObservable
    
    var cancellable: AnyCancellable?
    
    init(observable: SomeObservable) {
        self.observable = observable
        
        cancellable = observable.publisher.sink{ [weak self] in
            guard let self = self else {
                return
            }
            
            self.doSomethingWhenObservableChanges() // Must be a class to access self here.
        }
    }
    
    // How to call this function when "observable" changes?
    func doSomethingWhenObservableChanges() {
        print("Triggered!")
    }
}

这里要注意,如果管道末端没有注册sink或者receiver,那么这个值就会丢失。例如创建 PassthroughSubject<T, Never>,立即发送一个值并在之后返回发布者会使发送的消息丢失,尽管您随后在该主题上注册了一个接收器。通常的解决方法是将主题创建和消息发送包装在一个 Deferred {} 块中,一旦接收器注册,它只会在其中创建所有内容。

一位评论者指出 ReceiveOutsideView.observableReceiveOutsideView 所有,因为 observable 是在内部创建并直接赋值的。重新初始化时,将创建 observable 的新实例。在这种情况下,可以通过使用 @StateObject 而不是 @ObservableObject 来防止这种情况。