`@Published var name: ClassType` 在 SwiftUI 的_外部_/手动触发器不起作用?

`@Published var name: ClassType` doesn't work _outside_ of SwiftUI / manual trigger?

我发现了很多与此相关的 SwiftUI 主题,但没有帮助(例如

这不适用于 Swift 中的 Combine(特别是 not 使用 SwiftUI):

class SomeTask {
  @Published var progress = Progress(totalUnitCount: 5) // Progress is a Class
  [...]
}
var task = SomeTask()
let cancellable = task.$progress.sink { print([=10=].fractionCompleted) }
task.progress.completedUnitCount = 2

这与 SwiftUI 无关,因此没有 ObservableObject 继承来获得 objectWillChange,但即使我尝试使用 ObservableObjecttask.objectWillChange.send() 它什么也没做,尝试添加 extension Progress: ObservableObject {} 也无济于事。 由于发布者通过 var 的 willSet 发出值,并且由于 Progress 本身就是 class-类型,因此没有任何反应。

貌似没有真正像样的手动触发方式?

我发现只有解决方案是重新分配自己,这很尴尬:

let pr = progress progress = pr

(写progress = progress是编译时错误)。

唯一可行的方法可能是使用 Key-value-observing/KVO and/or 编写新的 @PublishedClassType 属性 包装器?

您可以尝试使用 CurrentValueSubject<Progress, Never>:

class SomeTask: ObservableObject {
    var progress = CurrentValueSubject<Progress, Never>(Progress(totalUnitCount: 5))

    func setProgress(_ value: Int) {
        progress.value.completedUnitCount = value
        progress.send(progress.value)
    }
}
var task = SomeTask()
let cancellable = task.progress.sink { print([=11=].fractionCompleted) }
task.setProgress(3)
task.setProgress(1)

这样你的Progress仍然可以是class

我能够使用 KVO 实现这个,由 @propertyWrapper 包装,CurrentValueSubject 作为发布者:

@propertyWrapper
class PublishedClass<T : NSObject> {
    private let subject: CurrentValueSubject<T, Never>
    private var observation: NSKeyValueObservation? = nil

    init<U>(wrappedValue: T, keyPath: ReferenceWritableKeyPath<T, U>) {
        self.wrappedValue = wrappedValue
        subject = CurrentValueSubject(wrappedValue)
        observation = wrappedValue.observe(keyPath, options: [.new]) { (wrapped, change) in
            self.subject.send(wrapped)
        }
    }

    var wrappedValue: T

    var projectedValue: CurrentValueSubject<T, Never> {
        subject
    }

    deinit {
        observation.invalidate()
    }
}

用法:

class Bar : NSObject {
    @objc dynamic var a: Int
    init(a: Int) {
        self.a = a
    }
}

class Foo {
    @PublishedClass(keyPath: \.a)
    var bar = Bar(a: 0)
}

let f = Foo()
let c = f.$bar.sink(receiveValue: { x in print(x.a) })
f.bar.a = 2
f.bar.a = 3
f.bar.a = 4

输出:

0
2
3
4

使用KVO的缺点当然是你传入的key路径必须是@objc dynamic并且keypath的root必须是NSObject的子类。 :(

我没试过,但如果你愿意,应该可以扩展它以观察多个关键路径。

基于我的想法,我确实实现了一个 @PublishedKVO 属性 包装器并将其作为一个小的 swift 包放在 github 上,支持多个关键路径。

https://github.com/matis-schotte/PublishedKVO

可用作:

class Example {
    @PublishedKVO(\.completedUnitCount)
    var progress = Progress(totalUnitCount: 2)

    @Published
    var textualRepresentation = "text"
}

let ex = Example()

// Set up the publishers
let c1 = ex.$progress.sink { print("\([=10=].fractionCompleted) completed") }
let c1 = ex.$textualRepresentation.sink { print("\([=10=])") }

// Interact with the class as usual
ex.progress.completedUnitCount += 1
// outputs "0.5 completed"

// And compare with Combines @Published (almost°) same behaviour
ex.textualRepresentation = "string"
// outputs "string"

ex.$progress.emit() // Re-emits the current value
ex.$progress.send(ex.progress) // Emits given value