sergdort / CleanArchitectureRxSwift

Example of Clean Architecture of iOS app using RxSwift

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Avoid relying on side effects of subscription in View layer

mrtj opened this issue · comments

Hi @sergdort, first of all thank you very much for your inspiring work. I am in the process of adopting some of the architectural patterns from your project and wanted to discuss with you my concern.

In PostViewController you are subscribing to createPost and selectedPost of the view model without any actual observer code passed. It is clear for me that this is done that the delegate style callback to the navigator hidden in the view model's do operator would be called -- without subscription neither the do is called. I have a kind of bad feeling about this solution because it is not clear in the view controller why one should create an empty subscription as the side effect of this subscription (i.e. calling navigator.toPost) is hidden in the view model. Do you think there is a way to refactor this code?

Hi, @mrtj. Yeah I agree, and it's bothers me as well :)

On the current project we are using very interesting approach, will try to find some time to implement this.
Would love to hear your ideas as well.

Well, I came up a solution that does not completely satisfy me but "it works".

First of all I think we need to separate the concept of ViewModel's Output into two parts:

  • in the first part there are the data sequences that should drive the view (for this I left the name Output)
  • in the second part there are event sequences that should be handled by somebody else, hierarchically above the ViewModel. In your PostViewModel these would be createPost and selectedPost. I created a new structure for these and I called it Events.

Now the problem is that in your architecture the View is calling the transform function of the ViewModel and the Events structure is created only then. How could we inject the Events back to the higher levels, in this case the PostsNavigator? (I followed the MVVM + Coordinators pattern by Lukasz but it basically does not matter from this point of view). The way with that I currently came up is the good old delegate pattern:

protocol ViewModelDelegate {
    func viewModel(_ viewModel: ViewModel, 
                   didCreate eventSources: ViewModel.Events, 
                   viewLifecycle: DisposeBag)
}

As it is said Events contains the drivers that produce the high-level events. The ViewModel takes an optional delegate in the initializer (implemented by the Navigator or the Coordinator), and in the transform function, before returning, calls the delegate with the newly created Events structure. The only interesting thing here is the viewLifecycle dispose bag. This allows the navigator to dispose its subscriptions to the event drivers when the view will be deallocated, and it is simply the dispose bag of the View. The View passes it as an additional parameter of the transform function and the ViewModel passes it to the delegate:

public func transform(input: Input, viewLifecycle: DisposeBag) -> (Output, Events) {
    let output = // ...
    let events = // ...
    delegate?.viewModel(self, didCreate: events, viewLifecycle: viewLifecycle)
    return (output, events)
}

(Note: I return the events also from the transform function because it makes unit testing much simpler)

Comments are welcome! 😉

Hello @sergdort, any updates on this?

commented

KISS - add disposeBag private var into viewModel class, assign it in transform function and after that (in same transform function) subscribe to createPost trigger driver. This should be hidden implementation and additional dispose bag is not such a big deal.

Comments are also welcome! :)

@sergdort nice work dude!

Hi, guys :) Sorry for the delay, kinda burned out off doing OSS

So in our app, we started to use RxFeedback and we are very happy about it.
We use Flow protocol as an abstraction on how VC can be presented we are going to open source it soon

Everything looks something like

final class FlowController {
    func handle(route: Route)
}
final class ViewModel {
    let state: Driver<State>
    let routes: Signal<Route>
}

final class Builder {
    func makeViewController() -> UIViewController {
        let flowController = FlowController(pushFlow: Flow, modalFlow: Flow)
        let viewModel = ViewModel(dependency: dependency)
        flowController.subscribeNext(viewModel.routes)

        return  ViewController(viewModel: viewModel)
    }
}

Thank you @sergdort, surely we'll have a look at it!

Thanks @sergdort, I've recently started to use RxFeedback in our project and I am very happy with it too. I am facing some difficulties with navigation and how to pass data between screens/states. Any idea on when you plan to opensource Flow? In your example above, where do you define the feedback loops and the "system" operator of RxFeedback?

An other solution could be flatMap the outputs with do side effects to .empty() (hence ignoring all the elements) and merge all of them in an output where the View is going to subscribe/drive. In this way there's no need to pass around a DisposeBag.

Example taking in consideration the PostsViewModel:

struct Output {
    let fetching: Driver<Bool>
    let posts: Driver<[PostItemViewModel]>
    let error: Driver<Error>
}

and in transform just return

return Output(
    fetching: fetching,
    posts: Driver.merge(
        posts,
        selectedPost.flatMap { _ in .empty() },
        createPost.flatMap { _ in .empty() }
    ),
    error: errors
)