是否有替代 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 的“新项目事件”时,您会怎么做,并且应该重新加载您的表/集合?在表格视图的 numberOfRowsInSectioncellForRowAt 方法中,您无法使用 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&lt;Void,Never&gt;,就像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 中传播初始值

使用 SwiftUI 和 Combine 根据授权状态有条件地显示视图?

组合:监听内部集合变化

HDU 5707 Combine String(动态规划)