我想使用Combine 的@Published
属性来响应属性的变化,但它似乎在属性发生变化之前发出信号,就像willSet
观察者一样。以下代码:
import Combine
class A {
@Published var foo = false
}
let a = A()
let fooSink = a.$foo.dropFirst().sink { _ in // `dropFirst()` is to ignore the initial value
print("foo is now \(a.foo)")
}
a.foo = true
Run Code Online (Sandbox Code Playgroud)
输出:
foo 现在是假的
我希望接收器像didSet
观察者一样在属性发生变化后运行,这样foo
就可以了。有没有其他出版商发出信号,或者有一种制作这样的@Published
作品的方式?
sur*_*der 14
Swift 论坛上有一个关于此问题的帖子。Tony_Parker解释了他们决定在“willSet”而不是“didSet”上发射信号的原因
我们(和 SwiftUI)选择 willChange 因为它比 didChange 有一些优势:
- 它允许对对象的状态进行快照(因为您可以通过属性的当前值和您收到的值访问旧值和新值)。这对于 SwiftUI 的性能很重要,但还有其他应用程序。
- “will”通知更容易在低级别合并,因为您可以跳过进一步的通知,直到发生其他事件(例如,运行循环旋转)。结合使用诸如removeDuplicates之类的运算符可以使合并变得简单,尽管我确实认为我们需要更多的分组运算符来帮助完成诸如运行循环集成之类的事情。
- 使用 did 获取半修改的对象更容易犯这样的错误,因为一项更改已完成,但另一项更改可能尚未完成。
当我收到一个值时,我并不直观地理解我收到的是 willSend 事件而不是 didSet 事件。对我来说这似乎不是一个方便的解决方案。例如,当在 ViewController 中收到来自 ViewModel 的“新项目事件”时,您会做什么,并且应该重新加载您的表/集合?在表视图numberOfRowsInSection
和cellForRowAt
方法中,您无法访问新项目,self.viewModel.item[x]
因为它尚未设置。在这种情况下,您必须创建一个冗余状态变量,仅用于缓存receiveValue:
块内的新值。
也许这对 SwiftUI 内部机制有好处,但恕我直言,对于其他用例来说并不是那么明显和方便。
用户clayellis在上面的线程中提出了我正在使用的解决方案:
发布者+didSet.swift
extension Published.Publisher {
var didSet: AnyPublisher<Value, Never> {
self.receive(on: RunLoop.main).eraseToAnyPublisher()
}
}
Run Code Online (Sandbox Code Playgroud)
现在我可以像这样使用它并获取 didSet 值:
self.viewModel.$items.didSet.sink { [weak self] (models) in
self?.updateData()
}.store(in: &self.subscriptions)
Run Code Online (Sandbox Code Playgroud)
不过,我不确定它对于未来的合并更新是否稳定。
UPD:值得一提的是,如果您从与主线程不同的线程设置值,则可能会导致错误(竞争)。
原主题链接:https://forums.swift.org/t/is-this-a-bug-in-published/31292/37 ?page=2
小智 7
在引入ObservableObject
SwiftUI 之前,它通常按照您指定的方式工作 - 它会在更改后通知您。对 的更改willChange
是有意进行的,并且可能是由某些优化引起的,因此使用ObservableObjsect
with@Published
总是会在设计更改之前通知您。当然,您可以决定不使用@Published
属性包装器并自己在回调中实现通知didChange
并通过属性发送它们objectWillChange
,但这将违反约定,并且可能会导致更新视图时出现问题。(https://developer.apple.com/documentation/combine/observableobject/3362556-objectwillchange)并且与 一起使用时会自动完成@Published
。如果您需要接收器进行用户界面更新以外的其他操作,那么我会实现另一个发布者,而不是违反惯例ObservableObject
。
您可以编写自己的自定义属性包装器:
import Combine
@propertyWrapper
class DidSet<Value> {
private var val: Value
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue value: Value) {
val = value
subject = CurrentValueSubject(value)
wrappedValue = value
}
var wrappedValue: Value {
set {
val = newValue
subject.send(val)
}
get { val }
}
public var projectedValue: CurrentValueSubject<Value, Never> {
get { subject }
}
}
Run Code Online (Sandbox Code Playgroud)
除了 Eluss 很好的解释之外,我将添加一些有效的代码。您需要创建自己PassthroughSubject
的发布者,并在didSet
更改发生后使用属性观察器发送更改。
import Combine
class A {
public var fooDidChange = PassthroughSubject<Void, Never>()
var foo = false { didSet { fooDidChange.send() } }
}
let a = A()
let fooSink = a.fooDidChange.sink { _ in
print("foo is now \(a.foo)")
}
a.foo = true
Run Code Online (Sandbox Code Playgroud)