在 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
。这里的发布者不发送任何内容,因此类型为<Void, Never>
。
计时器发布者
从定时器获取发布者可以直接通过Combine
或通过PassthroughSubject<Void, Never>()
创建通用发布者,注册消息并通过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<T, Never>
,立即发送一个值,然后返回发布者,这会使发送的消息丢失,尽管您随后在该主题上注册了接收器。通常的解决方法是将主题创建和消息发送包装在 Deferred
块中,该块仅在接收器注册后才在其中创建所有内容。
评论者指出ReceiveOutsideView.observable
归ReceiveOutsideView
所有,因为 observable 是在内部创建并直接分配的。在重新初始化时,将创建一个新的 observable
实例。在这种情况下,可以通过使用 @StateObject
而不是 @ObservableObject
来防止这种情况发生。
【讨论】:
这比我想象的要复杂一些——但你成功了。根据您的回答,我用一个工作示例更新了我的问题。感谢您的时间和解释,其中还包括Timer publisher
... 非常有用。 ??
如果可以从常规代码更新视图之外的@Published 值,则更为重要。假设我正在从数据库中读取一个值,并希望在视图中更新该值。
这就是ObservableObject
的用途,所以是的。 Published
的特殊之处在于它在正确的时间调用objectWillChange
。任何使用来自 ObservableObject
的值的视图都应该重新评估(我的最佳猜测)。
注意,随着 @StateObject ReceiveOutsideView 的引入,它的可观察属性应该是 StateObject 而不是 ObservableObject,因为它拥有/持有实例。
@lenny 好点,我更新了答案。虽然 StateObject >=5.3,所以不是每个人都可以使用它,但肯定会在未来使用。以上是关于在 SwiftUI 中,如何对“视图”的“@Published vars”*outside* 上的更改做出反应的主要内容,如果未能解决你的问题,请参考以下文章