除了 Combine 的 @Published 之外,还有其他方法可以在值发生变化之后而不是之前发出信号吗?

Is there an alternative to Combine's @Published that signals a value change after it has taken place instead of before?

我想使用 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 is now false

我希望在 属性 像 didSet 观察者一样发生变化后下沉至 运行,以便 foo 在该点上为真。是否有其他发布商发出信号,或者有办法让 @Published 像那样工作?

在引入 ObservableObject 之前,SwiftUI 曾经按照您指定的方式工作——它会在更改完成后通知您。对 willChange 的更改是有意进行的,可能是由某些优化引起的,因此将 ObservableObjsect@Published 一起使用将始终在设计更改之前通知您。当然,您可以决定不使用 @Published 属性 包装器并在 didChange 回调中自己实现通知并通过 objectWillChange 属性 发送它们,但是这将违反公约,并可能导致更新意见出现问题。 (https://developer.apple.com/documentation/combine/observableobject/3362556-objectwillchange) 并且在与 @Published 一起使用时自动完成。 如果您需要接收器进行 ui 更新以外的其他操作,那么我会实施另一个发布者,而不是再次违反 ObservableObject 约定。

除了 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

您可以编写自己的自定义 属性 包装器:

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 }
    }
}

Swift 论坛上有一个关于此问题的讨论帖。 Tony_Parker

解释了他们决定在“willSet”而不是“didSet”上发出信号的原因

We (and SwiftUI) chose willChange because it has some advantages over didChange:

  • It enables snapshotting the state of the object (since you have access to both the old and new value, via the current value of the property and the value you receive). This is important for SwiftUI's performance, but has other applications.
  • "will" notifications are easier to coalesce at a low level, because you can skip further notifications until some other event (e.g., a run loop spin). Combine makes this coalescing straightforward with operators like removeDuplicates, although I do think we need a few more grouping operators to help with things like run loop integration.
  • It's easier to make the mistake of getting a half-modified object with did, because one change is finished but another may not be done yet.

当我收到一个值时,我不直观地理解我收到的是 willSend 事件而不是 didSet。这对我来说似乎不是一个方便的解决方案。例如,当您在 ViewController 中收到来自 ViewModel 的“新项目事件”并且应该重新加载您的 table/collection 时,您会怎么做?在 table 视图的 numberOfRowsInSectioncellForRowAt 方法中,您无法使用 self.viewModel.item[x] 访问新项目,因为它尚未设置。在这种情况下,您必须创建一个冗余状态变量,仅用于在 receiveValue: 块中缓存新值。

也许它对 SwiftUI 内部机制有好处,但恕我直言,对于其他用例来说不是那么明显和方便。

用户 clayellis 在我正在使用的上述建议解决方案的线程中:

出版商+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)

不过我不确定它是否table 用于未来的 Combine 更新。

另一种选择是只使用 CurrentValueSubject 而不是具有 @Published 属性的成员变量。例如,以下内容:

@Published public var foo: Int = 10 

会变成:

public let foo: CurrentValueSubject<Int, Never> = CurrentValueSubject(10)

这显然有一些缺点,其中最重要的是您需要以 object.foo.value 而不是 object.foo 的形式访问值。但是,它确实为您提供了您正在寻找的行为。