#architecture #design #mobile #pattern

idea

202404171826-mvvm-c--overview.png Model-View View-Model Controller is a pattern for client applications which structures an app through a number of concepts:

App delegate

The app delegate is the dirty bit that starts the main coordinator. App delegate calls the start() of the AppCoordinator.

Coordinator

The Coordinator is responsible to handle navigation and relation between ViewControllers[1]. There is a hierarchy of coordinators: coordinator within a feature, and coordinators that orchestrate these features, and an app coordinator that orchestrate the entire thing[5].

The coordinator determines which screens should be shown[2]. It instantiates VCs, VMs, and all dependencies. It presents the VCs. Coordinators usually have a start and finish method[4]. They own the main UI component in which the VCs are working (e.g. AppCoordinator owns the main UIWindow), and the VCs themselves.

Coordinator injects a delegate to itself into the VC and VM to enable back communication[6]. (viewModel.coordinatorDelegate = self). Alternatively, the VM implements a ViewModelCoordinatorDelegate, which defines all events that the coordinator should respond to (passing updated view data as params), and the VC registers closures pointing to its own reaction to these events, or just storing the application state, and reaction on state change using didSet[17].

RootViewControllers are passed by the parent coordinator to its child. The child coordinator will add its VCs into the root VC in the start method (e.g. if this is a tab bar, add its own tab to it, if it's a window, present itself in it).

finish is used to clean up the VC, tell the service layer to cancel pending requests, etc. Then call the delegate to let it know it's finished (note: async model cannot be used because the lifecycle of finishing might be long and done only after a task is completed). It's nice to keep a list of children coordinators as well, to call their finish in cascade.

Coordinators handle navigation. A nice way of organizing it is to add an extension to the coordinator that handles all navigations that can happen (e.g. goToPersonList). To navigate, either of these might happen:

  1. the navigation instantiate or retrieve the relevant view controllers and view models, do the injection if necessary, then trigger the presentation of the VC.
  2. the navigation creates another sub-coordinator and starts it.

Coordinators handle callbacks through delegates. The parent coordinator implements its delegate as an extension (extension ParentCoordinator: ChildCoordinatorDelegate). The child's delegate is a protocol that lists the callbacks it needs to talk to its parent (e.g. didFinishFrom: should signal that the child is done and the parent should remove it from its children).

There will also be callbacks from the View models (not usually VCs unless VMs are not used properly...). E.g if the first step is to select a search type, then SearchCoordinator instantiates a SearchTypeSelector VM and VC to pick, then the SearchTypeSelectorVM will callback the SearchCoordinator through a SearchTypeSelectorCoordinatorDelegate that implements didSelect(option:SearchType) or any method required to pass its result. As a result, the coordinator might also dismiss those controllers and view models because their task is complete.

View models

View models implement all of the business logic of the feature9. There's a 1:1 relationship with a corresponding View Controller. The View Controller owns its View Model (it has a strong reference to it), but the VM is created by the Coordinator and injected into the VC.

The VM holds a weak reference to the view (The VC holds a strong one).

VMs are implementing an interface, and we're injecting them into VCs as interface rather than concrete type. This can allows to inject different VMs for testing or upgrading. There are usually 3 interfaces that a VM is implementing:

  1. The xxxViewModelType is the interface of the VM that the VC can call[11]. It has two main parts
    1. the data related stuff that holds all the formatted data that the view controller needs. These need to be formatted rather than sending models[14].
    2. the events related stuff that the ViewController will call to signify that something happened.
  2. The xxxViewModelViewDelegate [^13] is exposing the methods that the ViewController should implement to let the VC know that something happened (new data is available, etc.).
  3. The xxxViewModelCoordinatorDelegate[12] is exposing the callbacks that the coordinator needs to implement, and the ViewModel needs to be able to call to signify something happened.

Each of these protocols can be implemented in different extensions.

To hold a non-optional reference between the VM and VC, a neat trick is to us the didSet[15] method of vars to inject the VC as the viewDelegate of the VM.

View controllers

The single responsibility of ViewControllers is to handle the UI related stuff[10], they shouldn't touch any of the navigation related concern[3], or business logic9.

View data

View data is an adapter sent from the VM to the VC, that takes a model object and exposes a formatted interface so that the VC doesn't have to deal with formatting anything[16].

It's exposing a protocol that contains all the VC needs to display stuff, and inits with the model object.

The VM exposes that ViewData instead of the model directly, for example:

func tripCount() -> Int {
    return trips.count
}

func trip(forRow row: Int) -> TripViewDataType {
    let trip = trips[row]
    return TripViewData(trip: trip)
}

Services

Services are classes that provide an abstraction to View Models so that they can retrieve data/interact with external world. They are a front for APIs and data stores.

Checklist

links

Single Responsibility Principle (SRP)

references

iOS Architecture: MVVM-Cref

iOS Architecture: MVVM-C, Scenes (2/6)

I create a folder inside my Xcode project called Scenes which will contain subfolders for all the scenes. Inside a particular scene (as you can see in the Search example below) I have all of the parts pertaining to that scene

[1]: -

The Coordinator is going to be responsible for handling the navigation and relationships between ViewController’s.

iOS Architecture: MVVM-C, Coordinators (3/6)ref

[2]: -

A coordinator is an object (Class type in Swift) which has the sole responsibility, as it’s name implies, to coordinate the App’s navigation. Basically which screen should be shown, what screen should be shown next, etc.

[3]: -

A ViewController should not know what ViewController came before it, which one should come next, or what dependencies it should pass on. It should just be a “dumb” wrapper around the View/Subviews on the screen, and handle only UIKit related stuff.

[4]: -

If you want to create your own coordinator you first subclass this base class, and then override the start and finish methods. Then you have built in methods to handle adding and removing child coordinator’s. Very similar to how a UIViewController works.

[5]: -

Your app should consist of multiple coordinators, one for each scene. But it should always have one “main” AppCoordinator, which will be owned by the App Delegate.

[6]:

    lazy var searchInputViewModel: SearchInputViewModel! = {
        let viewModel = SearchInputViewModel()
        viewModel.coordinatorDelegate = self
        return viewModel
    }()

[7]: -

n this case we are implementing the didFinishFrom: delegate method of a child coordinator. Here if our child coordinator is finished, it’s finish method should be called, and it’s finish method should call it’s delegate to let it know it’s finished. Afterwards, we remove the child coordinator from our stack. That assures that it’s removed from memory.

[8]: -

These callbacks let us know if the user want’s to navigate to another part of the app, and from this is where we would call our goTo navigation methods. Notice how the ViewController this request is coming to is a paremeter in these delegate methods. This makes it much easier to be able to present view controllers on top of them without having to keep a reference or go looking for the view controller in our hierarchy.

iOS Architecture: MVVM-C, ViewModel’s (4/6)ref

Let’s have a thought experiment. Take a typical view controller and split it into two parts. On one side leave all of the UIKit/UIViewController specific methods and everything that deals directly with views and subviews. And on the the other side put everything else, all of your business logic; i.e. network requests, validation, preparing model data for presentation, etc…

So basically, the former part (UIKit stuff) will remain, as it should, in the view controller. Everything else, all of app specific business logic, will now be in the ViewModel.

[10]: -

Now the view controller which is a part of UIKit as it’s a UIViewController subclass only deals with UIKit stuff. Handling rotation, view loading, constraints, adding subviews, target actions, etc.

[11]: -

. YourNameViewModelType: This is the protocol that our ViewModel’s should implement. It is conformed of two main parts. The Data Source which will have all the formatted data the view controller needs, and the Events which are events that the ViewController will send up to us on any user action.

[12]: -

. YourNameViewModel_Coordinator_Delegate: This delegate protocol will let us bubble up any action’s that we can’t handle and must be handled by our coordinator. This delegate should be set by the coordinator when it creates each corresponding ViewModel.

[13]: -

YourName**ViewModel_View_Delegate This delegate protocol will let us communicate with the ViewController. Whenever we have new data for example, we let the ViewController know so it can update it’s screen. Since the view controller owns the ViewModel we don’t want to create a reference cycle by holding onto a strong reference of the view controller. So we have a delegate which is set by the view controller to itself when the ViewModel gets injected into it.

[14]: -

We don’t want to send any model object directly to the View layer so usually we will only return formatted String’s or other struct’s like ViewData’s

[15]: -


    var viewModel: LocationSearchViewModelType! {
        didSet {
            viewModel.viewDelegate = self
        }
    }

iOS Architecture: MVVM-C, ViewData’s (5/6)ref

[16]: -

a ViewData is just a dumb wrapper that takes in a model like for example a Trip and wraps it up in struct called TripViewData. This ViewData can then be sent from the ViewModel to the ViewController, without having to send the bare model

[17]: - MVVM-C in Practice - Overview (visualstudio.com)

// Data flowing through this VM.
protocol LiveLocationViewModelCoordinator {
    var onShare: (() -> Void)! { get set }
}

// Data flowing out of this VM.
protocol LiveLocationViewModelController {
    var onReceiveNewLocations: (([UserLiveLocation]) -> Void)! { get set }
}

protocol LiveLocationViewModelType: LiveLocationViewModelCoordinator, LiveLocationViewModelController {

    // Helpers
    var canShowUserLocation: Bool? { get }
    var currentLocation: CLLocation? { get }

    // Events
    func start()
}
final class LiveLocationViewModel: LiveLocationViewModelType {
    private let threadId: String
    private let service: LiveLocationService
    private let manager: CLLocationManager

    init(threadId: String, service: LiveLocationService, manager: CLLocationManager) {
        self.threadId = threadId
        self.service = service
        self.manager = manager
    }

    // Helpers
    var canShowUserLocation: Bool? { return type(of: manager).authorizationStatus() == .authorizedAlways }
    var currentLocation: CLLocation? { return manager.location }

    // Events
    func start() {
        service.registerForLiveLocation { [weak self] (liveLocations: [GetLocationsQuery.Data.GetLocation]) in
            self?.onReceiveNewLocations(liveLocations.userLiveLocations)
        }
    }

    // Data flowing through this VM
    var onShare: (() -> Void)!

    // Data flowing out of this VM
    var onReceiveNewLocations: (([UserLiveLocationData]) -> Void)!
}
struct UserLiveLocationData: Equatable {
    let location: CLLocation
    let identifier: String
    static func == (lhs: UserLiveLocation, rhs: UserLiveLocation) -> Bool {
        return lhs.identifier == rhs.identifier
    }
  self?.onReceiveNewLocations(liveLocations.userLiveLocations)

In the init of the VC:

viewModel.start()
viewModel.onReceiveNewLocations = { [weak self] (liveLocations: [UserLiveLocation]) in
            self?.userLiveLocations = liveLocations
}

Use didSet on the view data state of the VC to handle refresh UI:

// Binding
override var userLiveLocations: [UserLiveLocation]? {
    didSet {
        if userLiveLocations != oldValue {
            render()
        }
    }
}