Search code examples
swiftrx-swift

How to pass chain view controller presenter with observable


I'm new in the RxSwift development and I've an issue while presentation a view controller.

My MainViewController is just a table view and I would like to present detail when I tap on a item of the list.

My DetailViewController is modally presented and needs a ViewModel as input parameter.

I would like to avoid to dismiss the DetailViewController, I think that the responsability of dismiss belongs to the one who presented the view controller, i.e the dismiss should happen in the MainViewController.

Here is my current code

DetailsViewController

class DetailsViewController: UIViewController {

  @IBOutlet weak private var doneButton: Button!
  @IBOutlet weak private var label: Label!

  let viewModel: DetailsViewModel

  private let bag = DisposeBag()

  var onComplete: Driver<Void> {
    doneButton.rx.tap.take(1).asDriver(onErrorJustReturn: ())
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    setup()
    bind()
  }

  private func bind() {
    let ouput = viewModel.bind()

    ouput.id.drive(idLabel.rx.text)
      .disposed(by: bag)
  }

}

DetailsViewModel

class DetailsViewModel {

  struct Output {
    let id: Driver<String>
  }

  let item: Observable<Item>

  init(with vehicle: Observable<Item>) {
    self.item = item
  }

  func bind() -> Output {
    let id = item
      .map { $0.id }
      .asDriver(onErrorJustReturn: "Unknown")

    return Output(id: id)
  }

}

MainViewController

class MainViewController: UIViewController {

  @IBOutlet weak private var tableView: TableView!

  private var bag = DisposeBag()

  private let viewModel: MainViewModel

  private var detailsViewController: DetailsViewController?


  override func viewDidLoad(_ animated: Bool) {
    super.viewDidLoad(animated)
    bind()
  }

  private func bind() {
    let input = MainViewModel.Input(
      selectedItem: tableView.rx.modelSelected(Item.self).asObservable()
    )
    let output = viewModel.bind(input: input)
    showItem(output.selectedItem)
  }


  private func showItem(_ item: Observable<Item>) {
    let viewModel = DetailsViewModel(with: vehicle)
    detailsViewController = DetailsController(with: viewModel)

    item.flatMapFirst { [weak self] item -> Observable<Void> in
      guard let self = self,
            let detailsViewController = self.detailsViewController else {
        return Observable<Void>.never()
      }
      self.present(detailsViewController, animated: true)
      return detailsViewController.onComplete.asObservable()
    }
    .subscribe(onNext: { [weak self] in
      self?.detailsViewController?.dismiss(animated: true)
      self?.detailsViewController? = nil
    })
    .disposed(by: bag)
  }

}

MainViewModel

class MainViewModel {

  struct Input {
    let selectedItem: Observable<Item>
  }

  struct Output {
    let selectedItem: Observable<Item>
  }

  func bind(input: Input) -> Output {
    let selectedItem = input.selectedItem
      .throttle(.milliseconds(500),
                latest: false,
                scheduler: MainScheduler.instance)
      .asObservable()

    return Output(selectedItem: selectedItem)
  }

}

My issue is on showItem of MainViewController. I still to think that having the DetailsViewController input as an Observable isn't working but from what I understand from Rx, we should use Observable as much as possible.

Having Item instead of Observable<Item> as input could let me use this kind of code:

item.flatMapFirst { item -> Observable<Void> in
      guard let self = self else {
        return Observable<Void>.never()
      }
      let viewModel = DetailsViewModel(with: item)
      self.detailsViewController = DetailsViewController(with: viewModel)
      guard let detailsViewController = self.detailsViewController else {
        return Observable<Void>.never()
      }
      present(detailsViewController, animated: true)
      return detailsViewController
    }
    .subscribe(onNext: { [weak self] in
      self?.detailsViewController?.dismiss(animated: true)
      self?.detailsViewController = nil
    })
    .disposed(by: bag)

What is the right way to do this?

Thanks


Solution

  • You should not "use Observable as much as possible." If an object is only going to ever have to deal with a single item, then just pass the item. For example if a label is only ever going to display "Hello World" then just assign the string to the label's text property. Don't bother wrapping it in a just and binding it to the label's rx.text.

    Your second option is much closer to what you should have. It's a fine idea.

    You might find my CLE library interesting. It takes care of the issue you are trying to handle here.