在SwiftUI中,如何对“视图”的“外​​部”的“ @Published vars”上的更改做出反应

bac*_*h-f 5 swiftui

假设我有以下内容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))
    }
}
Run Code Online (Sandbox Code Playgroud)

还有一个View,它观察到:

struct SomeView: View {

    @ObservedObject var observable: SomeObservable

    var body: some View {
        Text(observable.information)
    }
}
Run Code Online (Sandbox Code Playgroud)

以上将按预期工作。
View重绘自己的时候ObservableObject变化:

viewUpdate

现在问题是

How could I do the same (say calling a function) in a "pure" struct that also observes the same ObservableObject? By "pure" I mean something that does not conform to View:

struct SomeStruct {

    @ObservedObject var observable: SomeObservable

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

(It could also be a class, as long as it's able to react to the changes on the observable.)

It seems to be conceptually very easy, but I'm clearly missing something.

(Note: I'm using Xcode 11, beta 6.)


Update (for future readers)

Here is a possible solution, based on the awesome answer provided by @Fabian:

import SwiftUI
import Combine

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()
    }
}

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

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.sink(receiveValue: { [weak self] _ in
            guard let self = self else { return }
            self.doSomethingWhenObservableChanges()
        })
    }

    func doSomethingWhenObservableChanges() {
        print(observable.information)
    }
}
Run Code Online (Sandbox Code Playgroud)

Result

结果

(Note: it's necessary to run the app in order to check the console output.)

Fab*_*ian 20

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

定时器发布者

要从计时器中获取发布者,可以直接通过Combine或通过创建通用发布者PassthroughSubject<Void, Never>()、注册消息并在timer-callbackvia 中发送它们来完成publisher.send()。该示例有两种变体。

ObjectWillChange 发布者

每个ObservableObject确实有一个.objectWillChange可以为它注册一个出版商sink为你做同样的Timer publishers。每次调用它或每次@Published变量更改时都应该调用它。但是请注意,这是在更改之前而不是在更改之后调用的。(DispatchQueue.main.async{}更改完成后在接收器内部进行反应)。

注册

每个接收器调用都会创建一个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!")
    }
}
Run Code Online (Sandbox Code Playgroud)

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

一个评论者注意到它ReceiveOutsideView.observable是由 拥有的ReceiveOutsideView,因为 observable 是在内部创建并直接赋值的。重新初始化时observable将创建一个新实例。在这种情况下,可以通过使用@StateObject代替来防止@ObservableObject这种情况。