首页 > 解决方案 > SwiftUI / Combine:监听数组项值变化

问题描述

我想显示多个文本字段,代表比赛每个部分的分数。

示例:对于排球比赛,我们有 25/20、25/22、25/23。全球得分为 3/0。

全局组件架构:

>> ParentComponent
    >> MainComponent
        >> X TextFieldsComponent (2 text fields, home/visitor score)

最低的组件 TextFieldsComponent 包含基本绑定:

struct TextFieldsComponent: View {
    @ObservedObject var model: Model

    class Model: ObservableObject, Identifiable, CustomStringConvertible {
        let id: String
        @Published var firstScore: String
        @Published var secondScore: String

        var description: String {
            "\(firstScore) \(secondScore)"
        }

        init(id: String, firstScore: String = .empty, secondScore: String = .empty) {
            self.id = id
            self.firstScore = firstScore
            self.secondScore = secondScore
        }
    }

    var body: some View {
        HStack {
            TextField("Dom.", text: $model.firstScore)
                .keyboardType(.numberPad)
            TextField("Ext.", text: $model.secondScore)
                .keyboardType(.numberPad)
        }
    }
}

父组件需要显示匹配所有部分的总分。我想尝试一个组合绑定/流来获得总分。

我尝试了多种解决方案,最终得到了这个不起作用的代码(reduce 似乎不是采用数组的所有元素,而是在内部存储了以前的结果):

struct MainComponent: View {
    @ObservedObject var model: Model
    @ObservedObject private var totalScoreModel: TotalScoreModel

    class Model: ObservableObject {
        @Published var scores: [TextFieldsComponent.Model]

        init(scores: [TextFieldsComponent.Model] = [TextFieldsComponent.Model(id: "main")]) {
            self.scores = scores
        }
    }

    private final class TotalScoreModel: ObservableObject {
        @Published var totalScore: String = ""
        private var cancellable: AnyCancellable?

        init(publisher: AnyPublisher<String, Never>) {
            cancellable = publisher.print().sink {
                self.totalScore = $0
            }
        }
    }

    init(model: Model) {
        self.model = model
        totalScoreModel = TotalScoreModel(
            publisher: Publishers.MergeMany(
                model.scores.map {
                    Publishers.CombineLatest($0.$firstScore, $0.$secondScore)
                        .map { ($0.0, $0.1) }
                        .eraseToAnyPublisher()
                }
            )
            .reduce((0, 0), { previous, next in
                guard let first = Int(next.0), let second = Int(next.1) else { return previous }
                return (
                    previous.0 + (first == second ? 0 : (first > second ? 1 : 0)),
                    previous.1 + (first == second ? 0 : (first > second ? 0 : 1))
                )
            })
            .map { "[\($0.0)] - [\($0.1)]" }
            .eraseToAnyPublisher()
        )
   }

    var body: some View {
        VStack {
            Text(totalScoreModel.totalScore)
            ForEach(model.scores) { score in
                TextFieldsComponent(model: score)
            }
        }
    }
}

我正在寻找一种解决方案来获取每个绑定更改的事件,并将其合并到一个流中,以在 MainComponent 中显示它。

N/B:TextFieldsComponent 也需要独立使用。

标签: swiftswiftuireactivecombine

解决方案


MergeMany是正确的方法,因为你自己开始了,虽然我认为你把事情复杂化了。

如果您想在视图中显示总分(假设总分由Model而不是“拥有” TotalScoreModel,这是有道理的,因为它拥有基础分数),那么您需要表明该模型将在何时更改任何基础分数都会改变。

然后您可以将总分作为计算属性提供,SwiftUI 将在重新创建视图时读取更新后的值。

class Model: ObservableObject {
   @Published var scores: [TextFieldsComponent.Model]

   var totalScore: (Int, Int) {
      scores.map { ($0.firstScore, $0.secondScore) }
            .reduce((0,0)) { $1.0 > $1.1 ? ( $0.0 + 1, $0.1 ) : ($0.0, $0.1 + 1) }
   }

   private var cancellables = Set<AnyCancellable>()

   init(scores: [TextFieldsComponent.Model] = [.init(id: "main")]) {
      self.scores = scores
      
      // get the ObservableObjectPublisher publishers
      let observables = scores.map { $0.objectWillChange } 

      // notify that this object will change when any of the scores change
      Publishers.MergeMany(observables)
         .sink(receiveValue: self.objectWillChange.send)
         .store(in: &cancellables)
   }
}

然后,在视图中,您可以Model.totalScore像往常一样使用:

@ObservedObject var model: Model

var body: some View {
   Text(model.totalScore)
}

推荐阅读