SwiftUI / Combine: прослушивание изменения значений элементов массива - PullRequest
0 голосов
/ 16 июня 2020

Я хочу отображать несколько текстовых полей, представляющих счет каждой части матча.

Пример: для волейбольного матча у нас есть 25/20, 25/22, 25/23. Глобальная оценка - 3/0.

Архитектура глобальных компонентов:

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

Самый низкий компонент, TextFieldsComponent, содержит базовые c привязки:

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)
        }
    }
}

Родительский компонент должен отображать общий счет всех частей матча. И я хотел попробовать привязку / поток Combine, чтобы получить общий балл.

Я пробовал несколько решений, и в итоге я получил этот нерабочий код (сокращение, похоже, не берет все элементы массива, но внутренне сохраняет предыдущий результат):

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.

Н / Б: компонент TextFieldsComponent также должен использоваться автономно.

1 Ответ

2 голосов
/ 17 июня 2020

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)
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...