【问题标题】:Is there an alternative to Combine's @Published that signals a value change after it has taken place instead of before?是否有替代 Combine 的 @Published 来表示值发生变化之后而不是之前的变化?
【发布时间】: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 像这样工作?

【问题讨论】:

    标签: swift combine


    【解决方案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。但是,它确实为您提供了您正在寻找的行为。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-05-01
          • 2013-10-29
          • 2020-08-17
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多