在 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)
}
}
以上将按预期工作。
View
在 ObservableObject
更改时重绘自身:
现在开始提问:
我怎么能在 "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.observable
归 ReceiveOutsideView
所有,因为 observable 是在内部创建并直接赋值的。重新初始化时,将创建 observable
的新实例。在这种情况下,可以通过使用 @StateObject
而不是 @ObservableObject
来防止这种情况。
假设我有以下 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)
}
}
以上将按预期工作。
View
在 ObservableObject
更改时重绘自身:
现在开始提问:
我怎么能在 "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.observable
归 ReceiveOutsideView
所有,因为 observable 是在内部创建并直接赋值的。重新初始化时,将创建 observable
的新实例。在这种情况下,可以通过使用 @StateObject
而不是 @ObservableObject
来防止这种情况。