Moving from MVP to MVVM (an iOS architecture part VI)

The aim of this post is just to explain how improve MVP approach in view layer. MVP is a great improving respect to MVC, clarifies presenter and unleads viewcontroller responsabilites. But at the time of testing is hard, but not impossible, to perform.

MVP approach

On MVP, presenter has the following is the connection between:

  • Views: provides models to be presented in de views and collect user interaction events from views.
  • Interactor: When some data has to be fecthed from Data Layer, or some task has to be performed in Domain Layer.
  • Coordinator: When presenter has finished to perform its work and has to notifiy it to coordinator.

👍Good point is that is clear presenter responsabilites, 👎down side is that this approach is not quite ‘single responsability’ (SOLID).

Moving MVP to MVVM is same goal as was moving from MVC to MVP. All in all is dividing in classes with more concrete responsabilities and, what is more important, easier to test.

MVVM approach

Even though presenter is still interacting with 3 componets, has been unleaded significantly. Most of its previous logic in MVP has been moved to View Model component.

View Model now will be responsible of dealing with interactor.

It has one (or many) inputs that are basically the UI events that presenter bypasses from views and a unique output that informs about view model state and optionally data models for being refreshed in the view. This is view model state:


enum TransactionsListViewModelState {
    case fetching
    case fetched([String])
}

The body of view model would be:


class TransactionsListViewModel {
    var onStateChanged: ((TransactionsListViewModelState) -> Void) = { _ in /* Default empty state */ }

    // MARK: - Private attributes
    var injectedTransactionsUseCase:TransactionsUseCaseProtocol

    init(transactionsUseCase:TransactionsUseCaseProtocol = TransactionsUseCase()) {
        self.injectedTransactionsUseCase = transactionsUseCase
    }
    
    func start() {
        onStateChanged(.fetching)
        injectedTransactionsUseCase.transactions(onComplete: { [weak self] transactionIds in
            guard let weakSelf = self else { return }
            weakSelf.onStateChanged(.fetched(transactionIds))

            })
    }
}

From the callback onStateChanged the view model will report presenter about changes in the view model.

In this example presenter request view model to fetch some data via start() method. The first thing that does start() is report to presenter .fetching, so presenter can show an activity indicator. Once data is retrieved from interactor is reported to presenter .fetched state and the data for being presented. So presenter, will remove activity indicator and will set data to its views.

Testing

In view model interactor constructor one of its optional parameters is the interactor that will be used. By default is initialized by the default with the properly one, but could be injected a mocked one or emulating errors.

On the other hand presenter bypasses UI events directly to view model so is possible emlulate user interactions in the test cases.

So view model coverage could reach 100% without problems, this is something that is not so easy in MVP approach.

This is an example of unit test:


func test_fetchTransactionIds() {
        let asyncExpectation = expectation(description: "\(#function)")

        let transactionsListViewModel = TransactionsListViewModel()
        let expectedSeq:[TransactionsListViewModelState] = [TransactionsListViewModelState.fetching,
                                                   TransactionsListViewModelState.fetched([])]
        var index = 0
        transactionsListViewModel.onStateChanged = { pollResultsViewModelState in
            guard index < expectedSeq.count else {
                XCTFail()
                asyncExpectation.fulfill()
                return
            }
            // Only check state, not the associated value (that's why .rawvalue)
            guard expectedSeq[index].rawValue == pollResultsViewModelState.rawValue else {
                XCTFail()
                asyncExpectation.fulfill()
                return
            }

            index += 1
            if index == expectedSeq.count {
                asyncExpectation.fulfill()
            }
        }
        transactionsListViewModel.start()
        self.waitForExpectations(timeout: self.timeout, handler: nil)
    }

Test basically validates that sequence of states received from the view model is the expected one and also the data contained on each state.

Conclusion

This implementation leaves the presenter, usually implemented as a view-controller, implemented with the minimum logic 🥗. It forwards ↪️ the events from UI to View Model, and ask to refresh its views depending on the state received by ViewModel. Once is done 🏁, gives the token to its coordinator who will responsible for presenting another presenter.

Related Post

First chapter of an iOS architecture

References

Here you can find a sample project using this pattern.