Home > Enterprise >  Swift Combine - Observe change of object's property publisher inside an array of objects
Swift Combine - Observe change of object's property publisher inside an array of objects

Time:11-02

I have a following structure:

class FileUploadCellViewModel {
    @Published var isUploaded: Bool = false
}

class FileUploadScreenViewModel: ObservableObject {
    @Published var viewModels: [FileUploadCellViewModel] = []
    @Published var isSendButtonEnabled: Bool = false
    private var cancellables: Set<AnyCancellable> = []

    init() {
        let publisher = $viewModels
            // ?? return a single publisher with `true` if all `isUploaded` are `true`
        }
        let isDateCorrectPublisher = ...

        publisher
            .combineLatest(isDateCorrectPublisher)
            .sink {
                isSendButtonEnabled = $0 && $1
            }
            .store(in: &cancellables)
    }
}

let screen = FileUploadScreenViewModel()
let viewModel1 = FileUploadCellViewModel()
let viewModel2 = FileUploadCellViewModel()

screen.viewModels.append(viewModel1) // expected: isSendButtonEnabled: false
screen.viewModels.append(viewModel2) // expected: isSendButtonEnabled: false

viewModel1.isUploaded = false // expected: isSendButtonEnabled: false
viewModel2.isUploaded = true // expected: isSendButtonEnabled: false
viewModel1.isUploaded = true // expected: isSendButtonEnabled: true

How do I observe each insertion into the viewModels array and after an element is inserted, observe its isUploaded property?

I have found a convenience tool to collect an array of publishers, maybe it would be helpful here.

CodePudding user response:

You are on the right track with needing a CombineLatestCollection for combining all isUploaded values of all cell view models.

My answer uses the combineLatest operator from the article you've linked.

Once you've combined all publishers, you simply need to call allSatisfy on the [Bool] to see if all cell view models finished uploading.

Now the only part missing is updating the subscription whenever the viewModels array changes - you can do this by observing the $viewModel publisher and passing in the updated array to a method, which combines the isUploaded properties of the updated view models.

class FileUploadScreenViewModel {
  @Published var viewModels: [FileUploadCellViewModel] = []
  @Published var isSendButtonEnabled: Bool = false

  private var subscriptions = Set<AnyCancellable>()

  init() {
    // Whenever the viewModels array changes, set up the subscription on each element of the array
    $viewModels.sink { [weak self] viewModels in
      self?.bindIsSendButtonEnabled(viewModels: viewModels)
    }.store(in: &subscriptions)
  }

  // Combine all isUploaded values from each element of viewModels and update isSendButtonEnabled accordingly
  private func bindIsSendButtonEnabled(viewModels: [FileUploadCellViewModel]) {
    let areUploaded = viewModels
      .map(\.$isUploaded)
      .combineLatest
      .map { areUploaded in
        areUploaded.allSatisfy { isUploaded in
          isUploaded == true
        }
      }

    let isDateCorrectPublisher = Just(true)

    areUploaded
      .combineLatest(isDateCorrectPublisher)
      .map { $0 && $1 }
      .assign(to: &$isSendButtonEnabled)
  }
}
  • Related