如何在 Combine 中最好地创建 @Published 值的发布者聚合?
Posted
技术标签:
【中文标题】如何在 Combine 中最好地创建 @Published 值的发布者聚合?【英文标题】:How to best create a publisher aggregate of @Published values in Combine? 【发布时间】:2021-08-27 18:20:35 【问题描述】:鉴于@OberservableObject
s 的层次结构 - 我经常发现自己需要一个发布者来提供整个结构的某种更新聚合(下面的示例计算一个总和,但它可以是任何东西)
以下是我想出的解决方案 - 有点工作,但也不是...... :)
问题 #1:它看起来很复杂 - 我觉得我错过了一些东西......
问题 #2:它不起作用,因为顶部的 $foo 发布者确实在 foo 更改之前向 foo 发出更改,然后第二个 self.$foo 发布者中不存在这些更改(显示旧状态)。
有时我需要聚合与 swiftUI 视图更新同步 - 这样我就需要使用 @Published 值,而不需要在变量的 didSet 期间发出单独的发布者。
我没有找到好的解决方案...那么你们将如何解决这个问题?
class Foo:ObservableObject
@Published var bar:Int = 0
class Foobar:ObservableObject
@Published var foo:[Foo] = []
var sumPublisher:AnyPublisher<Int,Never>
// Whenever the foo array or one of the foo.bar values change
//
$foo
.map fooArray in
Publishers.MergeMany( fooArray.map foo in foo.$bar )
.switchToLatest()
// Calclulate a new sum by collecting and reducing all foo.bar values.
//
.map [unowned self] _ in
self.$foo // <--- in case of a foo change, this is still the unchanged foo, therefore not correct.
.map fooArray -> AnyPublisher<Int,Never> in
Publishers.MergeMany( fooArray.map foo in foo.$bar.first() )
.collect()
.map barArray -> Int in
barArray.reduce(0, $0 + $1 )
.eraseToAnyPublisher()
.switchToLatest()
.switchToLatest()
.removeDuplicates()
.eraseToAnyPublisher()
【问题讨论】:
【参考方案1】:问题 #2:@Published
在“willSet”而不是“didSet”上触发信号。
你可以使用这个扩展:
extension Published.Publisher
var didSet: AnyPublisher<Value, Never>
self.receive(on: RunLoop.main).eraseToAnyPublisher()
和
self.$foo.didSet
.map _ in
//...//
问题 #1: 也许是这样:
class Foobar:ObservableObject
@Published var foo:[Foo] = []
@Published var sum = 0
var cancellable: AnyCancellable?
init()
cancellable =
sumPublisher
.sink
self.sum = $0
var sumPublisher: AnyPublisher<Int,Never>
let firstPublisher = $foo.didSet
.flatMap array in
array.publisher
.flatMap $0.$bar.didSet
.map _ -> [Foo] in
return self.foo
.eraseToAnyPublisher()
let secondPublisher = $foo.didSet
.dropFirst(1)
return Publishers.Merge(firstPublisher, secondPublisher)
.map barArray -> Int in
return barArray
.map $0.bar
.reduce(0, $0 + $1 )
.removeDuplicates()
.eraseToAnyPublisher()
并进行测试:
struct FooBarView: View
@StateObject var fooBar = Foobar()
var body: some View
VStack
HStack
Button("Change list")
fooBar.foo = (1 ... Int.random(in: 5 ... 9)).map _ in Int.random(in: 1 ... 9) .map(Foo.init)
Text(fooBar.sum.description)
Button("Change element")
let idx = Int.random(in: 0 ..< fooBar.foo.count)
fooBar.foo[idx].bar = Int.random(in: 1 ... 9)
List(fooBar.foo, id: \.bar) foo in
Text(foo.bar.description)
.onAppear
fooBar.foo = [1, 2, 3, 8].map(Foo.init)
编辑:
如果你真的更喜欢使用@Published
(willSet
发布者),它会发送bar
的新值,因此你可以推导出foo
(数组)的新值:
var sumPublisher: AnyPublisher<Int, Never>
let firstPublisher = $foo
.flatMap array in
array.enumerated().publisher
.flatMap index, value in
value.$bar
.map (index, $0)
.map index, value -> [Foo] in
var newArray = array
newArray[index] = Foo(bar: value)
return newArray
.eraseToAnyPublisher()
let secondPublisher = $foo
.dropFirst(1)
return Publishers.Merge(firstPublisher, secondPublisher)
.map barArray -> Int in
barArray
.map $0.bar
.reduce(0, $0 + $1 )
.removeDuplicates()
.eraseToAnyPublisher()
【讨论】:
这种方法的问题是它与 UI 更新不同步?我将始终有 1 个循环延迟,其中状态处于转换状态 - 这并不理想。 您能否解释一下您希望使用这种方法会遇到什么样的问题? 如果我需要聚合来更新 UI... 例如。总和可以反映我在 UI 中绘制结果所需的空间。 UI 更新会由 ObservableObject 的 objectWillChange 发布者触发,但是发布者的数据只能在一个周期后才可用? @Michael:好的。然后将 didSet 扩展放在一边。我用新的尝试更新了我的答案。 @MichaelJ:这对你有帮助吗?【参考方案2】:到目前为止,这里最简单的方法是使用struct
而不是class
和@Published
:
struct Foo
var bar: Int = 0
然后你可以简单地创建一个计算属性:
class Foobar: ObservableObject
@Published var foo: [Foo] = []
var sum: Int
foo.map(\.bar).reduce(0, +)
// ...
对于 SwiftUI 视图,您甚至不需要将其设为发布者 - 当 foo
更改时,因为它是 @Published
,它会导致视图再次访问 sum
,这将给它重新计算的值.
如果你坚持它是一个发布者,这仍然很容易做到,因为foo
本身会在它的任何值Foo
发生变化时发生变化(因为它们是值类型struct
s):
var sumPublisher: AnyPublisher<Int, Never>
self.$foo
.map $0.map(\.bar).reduce(0, +)
.eraseToAnyPublisher()
有时,无论出于何种原因,都无法将class
更改为struct
(可能每个class
都有自己的可自我更新的生命周期)。然后您需要手动跟踪数组中Foo
对象的所有添加/删除(通过willSet
或didSet
),并订阅foo.bar
中的更改。
【讨论】:
是的 - 我们经常进行这些结构与类的讨论。在我的真实案例中,Foo 是一个基类,还有很多其他的东西在发生。 而且我的 UI 总是会在顶层更新……? 每当数组foo
发生变化时,顶层都会更新——例如,添加或删除元素。但是,大概,您想在现有元素的bar
属性更改时更新总和?如果是这样,您必须订阅该更改。
好吧,如果 Foo 是一个结构,那么正如您所指出的,改变 bar 会改变数组,整个 UI 会被重绘?以上是关于如何在 Combine 中最好地创建 @Published 值的发布者聚合?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Node.js 中最好地创建 RESTful API [关闭]
如何使用具有超过8个值的System.HashCode.Combine?