【问题标题】:How to best create a publisher aggregate of @Published values in Combine?如何在 Combine 中最好地创建 @Published 值的发布者聚合?
【发布时间】:2021-08-27 18:20:35
【问题描述】:

鉴于@OberservableObjects 的层次结构 - 我经常发现自己需要一个发布者来提供整个结构的某种更新聚合(下面的示例计算一个总和,但它可以是任何东西)

以下是我想出的解决方案 - 有点工作,但也不是...... :)

问题 #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()
    }
    
}

【问题讨论】:

    标签: swift swiftui combine


    【解决方案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)
                }
            }
        }
    }
    

    编辑:

    如果你真的更喜欢使用@PublishedwillSet 发布者),它会发送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 发生变化时发生变化(因为它们是值类型structs):

    var sumPublisher: AnyPublisher<Int, Never> {
       self.$foo
           .map { $0.map(\.bar).reduce(0, +) }
           .eraseToAnyPublisher()
    }
    

    有时,无论出于何种原因,都无法将class 更改为struct(可能每个class 都有自己的可自我更新的生命周期)。然后您需要手动跟踪数组中Foo 对象的所有添加/删除(通过willSetdidSet),并订阅foo.bar 中的更改。

    【讨论】:

    • 是的 - 我们经常进行这些结构与类的讨论。在我的真实案例中,Foo 是一个基类,还有很多其他的东西在发生。
    • 而且我的 UI 总是会在顶层更新……?
    • 每当数组foo 发生变化时,顶层都会更新——例如,添加或删除元素。但是,大概,您想在现有元素的bar 属性更改时更新总和?如果是这样,您必须订阅该更改。
    • 好吧,如果 Foo 是一个结构,那么正如您所指出的,改变 bar 会改变数组,整个 UI 会被重绘?
    猜你喜欢
    • 2019-11-08
    • 1970-01-01
    • 1970-01-01
    • 2020-08-05
    • 1970-01-01
    • 2020-11-06
    • 1970-01-01
    • 2021-07-10
    • 1970-01-01
    相关资源
    最近更新 更多