SwiftUI / Combine 订阅多个嵌套集合中的更新

SwiftUI / Combine subscribe to updates in multiple nested collections

我有一个 SummaryView 和一个 Report 作为 @State

A Report 是一种协议,其中包含用户可能想要进行的一些更改:

protocol Report {
    var changeGroups: [ChangeGroup] { get set }
}

有几种报告;个别报告作为结构实施:

struct RealEstateReport: Report {
    static let name = "Real Estate Report"
    
    var changeGroups = [ChangeGroup]()
}

A ChangeGroup 是一个结构,其中包含(除其他外)人类可读的摘要和一些提议的更改:

struct ChangeGroup: Identifiable {
    var summary: String
    var proposedChanges = [ProposedChange]()
}

A ProposedChangeclass,表示应用程序向用户建议的一个离散更改,默认情况下为 enabled

class ProposedChange: ObservableObject, Identifiable {
    @Published var enabled = true
    let summary: String

(在详细视图中,enabled 绑定到 Toggle,因此用户可以打开和关闭每个提议的更改。)

所以一个 Report 有很多 ChangeGroup,它们本身有很多 ProposedChange

我试图在 SummaryView:

中包含一些高级细节
struct SummaryView: View {
    @State var report: Report
    
    var body: some View {
        Text("Summary")
            .foregroundColor(…) // ???
    }

我希望 foregroundColor 是红色、黄色或绿色:

我读过一些关于 Combine 的内容,我认为我需要为每个 ChangeGroup 创建一个新的 Combine 订阅,并将其映射到每个 ProposedChange 的一个新的 Combine 订阅enabled 属性,当一个值发生变化时将值展平,并检查它们是否都相同。

我对我要使用的确切语法有点迷茫。而且结构似乎不会以相同的方式发布更改(我猜是因为结构是值类型与引用类型)。

如何根据上述逻辑设置Text视图的foregroundColor?

如果 ProposedChange 是结构而不是 class,您的问题将立即得到解决。除非它的实例有自己的生命周期,否则它们只是价值的持有者,所以在语义上应该是一个结构。

您的问题得到解决的原因是因为改变结构的 属性 会改变结构,因此 SwiftUI 知道重新计算视图,而对于 class 您需要订阅更改。

假设 ProposedChange 是一个结构:

struct ProposedChange {
    var enabled = true
    var summary: String
}

以下应该有效:

struct SummaryView: View {
    @State var report: Report
    
    var body: some View {
        Text("Summary")
            .foregroundColor(summaryColor) 
    }

    var summaryColor: Color {
       let count = report.changeGroups.flatMap { [=11=].proposedChanges }
                         .map { ([=11=].enabled ? 1 : 0, 1) }
                         .reduce((0, 0), { ([=11=].0 + .0, [=11=].1 + .1) })

       if count.0 == count.1 { return Color.green }
       else if count.0 == 0 { return Color.red }
       else { return Color.yellow }
    }
}

我最终将所有 enabled 标志映射到它们的发布者,使用 CombineLatest 运算符将它们全部组合起来,然后在值更改时重新计算:

class ViewModel: ObservableObject {
    enum BoolState {
        case allTrue, allFalse, mixed
    }
    
    @Published var boolState: BoolState?

    private var report: Report

    init(report: Report) {
        self.report = report
        
        report
            .changeGroups // [ChangeGroup] 
            .map { [=10=].proposedChanges } // [[ProposedChange]]
            .flatMap { [=10=] } // [ProposedChange]
            .map { [=10=].$enabled }  // [AnyPublisher<Bool, Never>]
            .combineLatest() // AnyPublisher<[Bool], Never>
            .map { Set([=10=]) } // AnyPublisher<Set<Bool>, Never>
            .map { boolSet -> BoolState in
                switch boolSet {
                case [false]:
                    return .allFalse
                case [true]:
                    return .allTrue
                default:
                    return .mixed
                }
            }  // AnyPublisher<BoolState, Never>
            .assign(to: &$boolState)
    }
}

Note: .combineLatest() is not part of Combine but it's just an extension I wrote that iterates each pair of publishers in the array and calls them iteratively, like first.combineLatest(second).combineLatest(third) etc. If you need something more robust than this, it looks like the CombineExt project has a CombineLatestMany extension with several options.

此时我的观点只是做了一个@ObservedObject var viewModel: ViewModel,然后在body中使用了viewModel.boolState。无论何时任何 enabled 标志因任何原因发生变化,视图都会成功更新!