是否有替代 Combine 的 @Published 来表示值发生变化之后而不是之前的变化?
Posted
技术标签:
【中文标题】是否有替代 Combine 的 @Published 来表示值发生变化之后而不是之前的变化?【英文标题】:Is there an alternative to Combine's @Published that signals a value change after it has taken place instead of before? 【发布时间】:2020-02-12 15:30:32 【问题描述】:我想使用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
输出:
foo 现在是假的
我希望接收器在属性更改后像 didSet
观察者一样运行,以便 foo
在那时为真。是否有其他发布者发出信号,或者让@Published
像这样工作?
【问题讨论】:
【参考方案1】:在 Swift 论坛上有一个关于这个问题的帖子。 Tony_Parker
解释了他们决定在“willSet”而不是“didSet”上触发信号的原因我们(和 SwiftUI)选择 willChange 是因为它比 更改:
它启用快照对象的状态(因为你 可以通过当前值访问旧值和新值 您收到的财产和价值)。这对于 SwiftUI 的性能,但还有其他应用程序。 “will”通知更容易在低级别合并,因为您可以 跳过进一步的通知,直到发生其他事件(例如,运行循环 旋转)。结合使用运算符使这种合并变得简单 像 removeDuplicates,虽然我确实认为我们需要更多的分组 运算符来帮助处理诸如运行循环集成之类的事情。 使用 did 获取半修改对象更容易出错, 因为一项更改已完成,但另一项更改可能尚未完成。
当我收到一个值时,我不直观地理解我收到的是 willSend 事件而不是 didSet。对我来说,这似乎不是一个方便的解决方案。例如,当您在 ViewController 中收到来自 ViewModel 的“新项目事件”时,您会怎么做,并且应该重新加载您的表/集合?在表格视图的 numberOfRowsInSection
和 cellForRowAt
方法中,您无法使用 self.viewModel.item[x]
访问新项目,因为它尚未设置。在这种情况下,您必须创建一个冗余状态变量,仅用于缓存 receiveValue:
块中的新值。
也许它对 SwiftUI 内部机制有好处,但恕我直言,对于其他用例来说不是那么明显和方便。
用户 clayellis 在上面的线程中提出了我正在使用的解决方案:
Publisher+didSet.swift
extension Published.Publisher
var didSet: AnyPublisher<Value, Never>
self.receive(on: RunLoop.main).eraseToAnyPublisher()
现在我可以这样使用它并获取 didSet 值:
self.viewModel.$items.didSet.sink [weak self] (models) in
self?.updateData()
.store(in: &self.subscriptions)
不过,我不确定它对于未来的 Combine 更新是否稳定。
【讨论】:
【参考方案2】:您可以编写自己的自定义属性包装器:
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
【讨论】:
这真的需要使用CurrentValueSubject
吗?在我看来, CurrentValueSubject 将始终具有与 wrappedValue
属性相同的值。为什么不使用PassthroughSubject<Void,Never>
,就像objectWillChange
一样?
两者都很好。 CurrentValueSubject 作为一个通用的解决方案更加通用。【参考方案3】:
在 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
【讨论】:
谢谢,这正是我最终所做的。大概可以将该模式封装在自定义属性包装器中(也许使用自定义发布者,但也许可以使用 PassthroughSubject 来完成)。 太棒了,我明白这一点并已使用而不是 @Publish【参考方案4】:在引入ObservableObject
之前,SwiftUI 曾经按照您指定的方式工作——它会在进行更改后通知您。对willChange
的更改是有意进行的,并且可能是由一些优化引起的,因此使用ObservableObjsect
和@Published
将始终在设计更改之前通知您。当然,您可以决定不使用@Published
属性包装器并在didChange
回调中自己实现通知,并通过objectWillChange
属性发送它们,但这将违反惯例并且可能会导致更新视图出现问题。 (https://developer.apple.com/documentation/combine/observableobject/3362556-objectwillchange) 并在与@Published
一起使用时自动完成。
如果您需要接收器用于 ui 更新以外的其他内容,那么我将实施另一个发布者,而不是再次遵循 ObservableObject
约定。
【讨论】:
【参考方案5】:另一种选择是只使用CurrentValueSubject
而不是具有@Published
属性的成员变量。例如,以下内容:
@Published public var foo: Int = 10
会变成:
public let foo: CurrentValueSubject<Int, Never> = CurrentValueSubject(10)
这显然有一些缺点,尤其是您需要以object.foo.value
的形式访问该值,而不仅仅是object.foo
。但是,它确实为您提供了您正在寻找的行为。
【讨论】:
以上是关于是否有替代 Combine 的 @Published 来表示值发生变化之后而不是之前的变化?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 Combine + Swift 复制 PromiseKit 风格的链式异步流
Delegate.Combine:如何检查多播(可组合)委托中是不是已经有委托?
触发 CombineLatest 在 Combine 中传播初始值