0

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

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
DEADBEEF
  • 1,930
  • 2
  • 17
  • 32

1 Answers1

0

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.

Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • Thank you for you reply @Daniel T. . I understand it better now. I will let the Observable since I need to make a api call when a new item is coming so I could use item.flatMap { apiCall() } – DEADBEEF Aug 12 '21 at 10:55
  • @DEADBEEF That's not a reason to use an observable either. You could always just make your apiCall outside the closure. Only wrap your type in an Observable if the value changes over time. If you can declare the value with `let`, then just do that instead. No reason to over-complicate your code. – Daniel T. Aug 12 '21 at 11:00
  • thank you again, well I guess it's what happen when you discover Rx you go full Rx :). So the thing will be to pass a regular type as input and create an Observable from this item in order to bind the UI with the result of the apiCall? – DEADBEEF Aug 12 '21 at 11:22