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

Posted

技术标签:

【中文标题】在 SwiftUI 中,如何对“视图”的“@Published vars”*outside* 上的更改做出反应【英文标题】:In SwiftUI, how to react to changes on "@Published vars" *outside* of a "View" 【发布时间】:2019-08-22 10:09:23 【问题描述】:

假设我有以下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)
    

以上将按预期工作。 当ObservableObject 发生变化时,View 会重新绘制自身:

现在回答问题

我怎么能在一个“纯”struct 中做同样的事情(比如调用一个函数),它也观察到相同的ObservableObject?我所说的“纯”是指符合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 中)

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

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

结果

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

【问题讨论】:

必须是结构体吗? 不一定。可以是一堂课。我会更新问题。 【参考方案1】:

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

计时器发布者

从定时器获取发布者可以直接通过Combine 或通过PassthroughSubject&lt;Void, Never&gt;() 创建通用发布者,注册消息并通过timer-callback 发送消息publisher.send()。该示例具有两种变体。

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!")
    

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

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

【讨论】:

这比我想象的要复杂一些——但你成功了。根据您的回答,我用一个工作示例更新了我的问题。感谢您的时间和解释,其中还包括Timer publisher... 非常有用。 ?? 如果可以从常规代码更新视图之外的@Published 值,则更为重要。假设我正在从数据库中读取一个值,并希望在视图中更新该值。 这就是ObservableObject 的用途,所以是的。 Published 的特殊之处在于它在正确的时间调用objectWillChange。任何使用来自 ObservableObject 的值的视图都应该重新评估(我的最佳猜测)。 注意,随着 @StateObject ReceiveOutsideView 的引入,它的可观察属性应该是 StateObject 而不是 ObservableObject,因为它拥有/持有实例。 @lenny 好点,我更新了答案。虽然 StateObject >=5.3,所以不是每个人都可以使用它,但肯定会在未来使用。

以上是关于在 SwiftUI 中,如何对“视图”的“@Published vars”*outside* 上的更改做出反应的主要内容,如果未能解决你的问题,请参考以下文章

如何将函数中的多个修饰符应用于 SwiftUI 中的给定视图?

如何使用 swiftUI 沿自定义路径移动视图/形状?

如何在函数中传入 SwiftUI 视图?

如何在 swiftUI 视图中使用按钮? [关闭]

如何在 SwiftUI 子视图中执行函数?

如何在 SwiftUI 中动态推送视图或呈现视图?