MVVM + C — A logic centered pattern

We should make it this way, and then the other way, and then another, and after this that thing should happen, but only the third time user taps this button in app lifetime, that would be good — every UX designer

While developing our newest app RocketLuncher we wanted to keep its code as clean as possible. We wanted to keep ViewControllers in the View development layer, which meant they should know nothing about app data. MVVM was a no-brainer. But as the app grew, a problem appeared: it should be logic-centered and the user data should be kept between different screens. Another requirement was to keep a hierarchy of ViewModels while developing the app. That’s when the subject of a Coordinator (also called FlowController) came up. A boosted, view-modeled one is the result of our complicated equation.

Overview:

The MVVM pattern. For us it was love at first sight. We learnt how to use and test it while creating the DaftCode app. The pattern is easy when state is nonexistent, but when some data and runtime state come into play — it gets complicated. The parent-child hierarchy with view models turned out to do the work for us. A view model would create a child when asked for and then keep it as its current child. The same object would be inserted in the view controller being presented.

Protocols as a hierarchy helper:

To make all this possible we used some powers of Swift. Protocols came in handy, as we were already using them for testing. They turned out to be helpful with keeping the hierarchy of view models.

Here’s an example to help you get it all:

  • We need to create a protocol for the child layer:
protocol FirstLayerChildViewModeling { }
  • Define some screen specific protocols, and make them also the previously defined protocol
protocol FirstScreenViewModeling: FirstLayerChildViewModeling {}
protocol SecondScreenViewModeling: FirstLayerChildViewModeling {}
//Those protocols may also define some API for the ViewControllers
//But that is how MVVM works
  • Define some classes for the above:
class FirstScreenViewModel: FirstScreenViewModeling {}
class SecondScreenViewModel: SecondScreenViewModeling {}
  • Make the parent able to return those and remember the current child:
class FirstLayerParentViewModel {
var currentChild: FirstLayerChildViewModeling?
    func getChildViewModel() -> FirstScreenViewModeling {
var viewModel: FirstScreenViewModeling = ...
currentChild = viewModel
return viewModel
}
    func getChildViewModel() -> SecondScreenViewModeling {
//same as above
}
}

Protocols are also helpful when you want to use different view models for one view controller in different cases.

Closures as a communication stream:

The hierarchy is kept, but children still need to communicate their changes to their parent. That’s when closures come to play. Let’s see an example using protocols defined before:

  • Define the closure in protocol as a variable
protocol FirstLayerChildViewModeling {
var onContextUpdate: ((FirstLayerContext) -> Void)? { get set }
}
  • Seems easy, now make classes conform to protocol:
//FirstScreenViewModeling is also FirstLayerChildViewModeling!!!
class FirstScreenViewModel: FirstScreenViewModeling {
var onContextUpdate: ((FirstLayerContext) -> Void)?
}
  • Insert the closure when the object is created:
class FirstLayerParentViewModel {
var currentChild: FirstLayerChildViewModeling?
    func getChildViewModel() -> FirstScreenViewModeling {
var viewModel: FirstScreenViewModeling = ...
viewModel.onContextUpdate = { [weak self] aContext in
self?.reactFor(updatedContext: aContext)
}
...
}
    func reactFor(updatedCotnext context: FirstLayerContext) {
...
}
    ...
}

In our case one stream wasn’t enough. We needed to react for both context updates and user actions such as button presses or dismissals. That can be accomplished simply by repeating the states above

Partial sum:

We used two features of Swift to make the MMVM+C pattern work for us:

  • protocols — parent object would always create an object implementing the child protocol,
  • closures — each child should communicate its changes to its parent via a closure inserted right after creation.

The state in view model:

  • In our implementation an object being a state machine was the view model (usually a parent one):
state FirstLayerState {
case firstScreen
case secondScreen
}
class FirstLayerParentViewModel {
...
var currentState: FirstLayerState = .firstScreen
...
}
  • Changes which happen in child view models are then sent to their parent where they are processed:
class FirstLayerParentViewModel {
...
func reactFor(updatedCotnext context: FirstLayerContext) {
... //process the context
currentState = .secondScreen
}
...
}

One context to rule them all:

RocketLuncher had some more complex context implementation (using protocols!), but the whole idea was to create a structure that would contain all data collected on different screens during app runtime:

struct FirstLayerContext {
var firstName: String? //from first screen
var secondName: String? //from first screen
var email: String? //from second screen
var genre: Genre? //from second screen
}

The coordinator!:

Finally, let’s talk about the main plot twist.

  • The Coordinator, which owns its view model and a view controller:
class FirstLayerCoordinator {
var viewModel: FirstLayerViewModeling
var viewController: UIViewController = UIViewController()
}
  • The view model communicates its state change:
protocol FirstLayerViewModeling {
var onStateChange: ((FirstLayerState, FirstLayerState) -> Void)?
}
class FirstLayerParentViewModel: FirstLayerViewModeling {
var onStateChange: ((FirstLayerState, FirstLayerState) -> Void)?
var currentState: FirstLayerState = .firstScreen {
didSet {
if currentState != oldValue {
onStateChange?(oldValue, currentState)
}
}
}
...
}
  • The coordinator reacts for it with UI change:
class FirstLayerCoordinator {
var viewModel: FirstLayerViewModeling
    ...
func set(viewModel: FirstLayerViewModeling) {
self.viewModel = viewModel
self.viewModel.onStateChange = { [weak self] old, new in
self?.transition(from: old, to: new)
}
}
    func transition(from: FirstLayerState, to: FirstLayerState) {
switch (from, to) {
case (.firstScreen, .secondScreen):
presentSecondScreen()
...
}
}
    func presentSecondScreen() {
...
}

}
  • The coordinator should insert supplied child view model and present the new view controller:
func presentSecondScreen() {
let x: SecondScreenViewModeling = viewModel.getChildViewModel()
let secondScreen = SecondScreenViewController(viewModel: x)
containerViewController.present(secondScreen, animated: true)
}
  • If things get more complex another coordinator may be created, everything else is similar
func presentRegistrationFlow() { 
let x: ThirdViewModeling = viewModel.getChildViewModel()
let y: UIViewController = viewController
let thirdCoordinator = ThirdCoordinator(viewModel: x, vc: y)
thirdCoordinator.present()
}
  • Neither view controllers nor coordinators dismiss themselves, they are dismissed by their parrent, and that makes the whole hierarchy work

That seems to be the whole magic in it.

The Pros:

  • Code separation — the pattern makes it effortless to separate objects responsible for flow, layout and business logic. As a result, different people can work on different parts of the project without worrying about merge conflicts.
  • Testability — as before, objects get smaller and get less responsibility through separation. TDD all the way!
  • Grow proofness — If you want to add a state, all you have to do is decide where to put it, and as the app gets bigger, the expansion doens’t get any harder.
  • Exchangeability — That is the power of Protocol Oriented MVVM: if you want the controller to react in some other way, you simply suppy it with some other view model. If want to change the flow, you simply change it in the view model responsible for corresponding changes.
  • Device expandability — When the app ought to show a different view controller while on iPad, it’s obvious to change the flow in a coordinator.

The Cons:

  • A big switch — the way the coordinator works is reacting to state change. The massive switch statement is unevidable, when views appear differently each time. (but at least you get thecontroll).
  • UINavigationController aka. the state killer — you have to make workarounds to react for the default back action of this beautiful controller supplied by UIKit
  • Change propagation — The worst drawback of the pattern is that when something changes in the root object, it usually means changing all child objects… and coordinators… and all the tests.. and fakes.
  • LOTS of files — coordinators, view models, view controllers and tests for it all makes your file tree grow, but that’s not that bad, right?

Summary (TL;DR):

The MVVM+C pattern seems complicated at first, but after a while it feels more natural. The state changes are not a part of the user interface, but they are shown to the user as view controllers. This pattern gives developers the control over what is and will be presented. It’s clean as it separates business logic from user interface. It makes development much more pleasureable.

Further reading:

Don’t forget to tap and hold 👏 and to follow DaftMobile Blog (just press “Follow” button below 😅)! You can also find us on Facebook and Twitter 🙂

Like what you read? Give Eryk Sajur a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.