How do you communicate between a UIViewController and its child UIView using MVVM and RxSwift events?

I'm using MVVM, Clean Architecture and RxSwift in my project. There is a view controller that has a child UIView that is created from a separate .xib file on the fly (since it is used in multiple scenes). Thus there are two viewmodels, the UIViewController's view model and the UIView's. Now, there is an Rx event in the child viewmodel that should be observed by the parent and then it will call some of its and its viewmodel's functions. The code is like this:

MyPlayerViewModel:

class MyPlayerViewModel {
    var eventShowUp: PublishSubject<Void> = PublishSubject<Void>()
    var rxEventShowUp: Observable<Void> {
        return eventShowUp
    }
}

MyPlayerView:

class MyPlayerView: UIView {
    var viewModel: MyPlayerViewModel?
    
    setup(viewModel: MyPlayerViewModel) {
        self.viewModel = viewModel
    }
}

MyPlayerSceneViewController:

class MyPlayerSceneViewController: UIViewController {
    @IBOutlet weak var myPlayerView: MyPlayerView!
    @IBOutlet weak var otherView: UIView! 

    var viewModel: MyPlayerSceneViewModel
    fileprivate var disposeBag : DisposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.myPlayerView.viewModel.rxEventShowUp.subscribe(onNext: { [weak self] in
            self?.viewModel.doOnShowUp()
            self?.otherView.isHidden = true
        })
    }
}

As you can see, currently, I am exposing the myPlayerView's viewModel to the public so the parent can observe the event on it. Is this the right way to do it? If not, is there any other suggestion about the better way? Thanks.

Upvotes: 2

Views: 1851

Answers (1)

Tomasz Pe
Tomasz Pe

Reputation: 716

In general, nothing bad to expose view's stuff to its view controller but do you really need two separate view models there? Don't you mix viewModel and model responsibilities?

Some thoughts:

  • Model shouldn't subclass UIView.
  • You should avoid creating own subjects in a view model. It doesn't create events by itself, it only processes input and exposes results.
  • I encourage you to get familiar with Binder and Driver.

Here is the code example:

struct PlayerModel {

    let id: Int
    let name: String
}

class MyPlayerSceneViewModel {

    struct Input {
        let eventShowUpTrigger: Observable<Void>
    }

    struct Output {
        let someUIAction: Driver<PlayerModel>
    }

    func transform(input: Input) -> Output {
        let someUIAction = input.eventShowUpTrigger
            .flatMapLatest(fetchPlayerDetails) // Transform input
            .asDriver(onErrorJustReturn: PlayerModel(id: -1, name: "unknown"))

        return Output(someUIAction: someUIAction)
    }

    private func fetchPlayerDetails() -> Observable<PlayerModel> {
        return Observable.just(PlayerModel(id: 1, name: "John"))
    }
}

class MyPlayerView: UIView {

    var eventShowUp: Observable<Void> {
        return Observable.just(()) // Expose some UI trigger
    }

    var playerBinding: Binder<PlayerModel> {
        return Binder(self) { target, player in
            target.playerNameLabel.text = player.name
        }
    }

    let playerNameLabel = UILabel()
}

class MyPlayerSceneViewController: UIViewController {

    @IBOutlet weak var myPlayerView: MyPlayerView!

    private var viewModel: MyPlayerSceneViewModel!
    private var disposeBag: DisposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupBindings()
    }

    private func setupBindings() {
        let input = MyPlayerSceneViewModel.Input(eventShowUpTrigger: myPlayerView.eventShowUp)
        let output = viewModel.transform(input: input)

        // Drive manually
        output
            .someUIAction
            .map { $0.name }
            .drive(myPlayerView.playerNameLabel.rx.text)
            .disposed(by: disposeBag)

        // or to exposed binder
        output
            .someUIAction
            .drive(myPlayerView.playerBinding)
            .disposed(by: disposeBag)
    }
}

Upvotes: 3

Related Questions