S(DI)-MVVM-C

@attheodo

An architecture for scalable and testable apps

Terms

S (DI)

Service (Dependency Injection)

MVVM

Model View - View Model

C

(Flow) Coordinators

The problem with MVC

View

Controller

Model

View

Controller

Model

}

Massive
View
Controller

Is this a model, a view or a controller? 🤔

Anything that isn't "data" or "graphic" gets thrown into the controller...

View Controller

Model-View Binding

Subview allocation

Data Fetching

Layout

Data transformation

Navigation Flow

Model Mutation

Device configuration

"But...I am just supposed to update the view based on the model..." - VC

😱

View Controller

Model-View Binding

Subview allocation

Data Fetching

Layout

Data transformation

Navigation Flow

Model Mutation

Device configuration

Coordinator

Service (Dependency)

View

View Model

User Input

Coordinators

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    let object = dataSource(objectAtIndexPath: indexPath)
    
    let nextVC = MyViewController()
    nextVC.object = object
    nextVC.someConfProperty = ...
    nextVC.someFlag = true
 
    self.navigationController.pushViewController(nextVC, animated: true)
   
}

El classico...

👌

😒

💩

Coordinators

protocol Coordinator: class {
    
    var rootViewController: UIViewController { get }
    
    func addChildCoordinator(_ child: Coordinator)
    func removeChildCoordinator(_ child: Coordinator)
    func removeAllChildCoordinators()
    func start()
    func stop()

}

✅ No present(:), dimiss(:), performSegue(:) calls in VCs any more

✅ VCs tell their coordinator what happened via delegation.

✅ Coordinator is responsible to inject services and configure VCs on initialization.

Coordinators

protocol Coordinator: class {
    
    var rootViewController: UIViewController { get }
    
    func addChildCoordinator(_ child: Coordinator)
    func removeChildCoordinator(_ child: Coordinator)
    func removeAllChildCoordinators()
    func start()
    func stop()

}

✅ No present(:), dimiss(:), performSegue(:) calls in VCs any more

✅ VCs tell their coordinator what happened via delegation.

✅ Coordinator is responsible to inject services and configure VCs on initialization.

Coordinators

class AppCoordinator: BaseCoordinator {
        
    init() {

        let libraryCoordinator = LibraryCoordinator()
        let profileCoordinator = ProfileCoordinator()

        let tabBarController = UITabBarController()

        tabBarController.viewControllers = [
            libraryCoordinator.rootViewController,
            profileCoordinator.rootViewController
        ]

        super.init(rootViewController: tabBarController)

        addChildCoordinator(libraryCoordinator)
        addChildCoordinator(profileCoordinator)

    }
    
}

Coordinators

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    var appCoordinator: AppCoordinator!

    func application(_ application: UIApplication, 
                     didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool 
{
        
        appCoordinator = AppCoordinator()
        
        let window = UIWindow()
        window.rootViewController = appCoordinator.start()
        window.makeKeyAndVisible()
        self.window = window

        return true
    }

}

Coordinators

App
Coordinator

Login
Coordinator

Profile
Coordinator

List
Coordinator

Register
Coordinator

Forgot Pass Coordinator

GetEmailVC

SubmitPinVC

ChangePasswordVC

delegation

Coordinators

✅ VCs are now isolated. They only present data.

✅ VCs are now reusable. They don't assume the context they are       presented in. You can mix & match them in any way.

✅ Coordinators are objects fully in control. No more relying on           viewDid*() & viewWill* methods

✅ You have a place to configure and style your higher-level                 objects like UITabBarController and UINavigationController

✅ More flexible decision making based on device and interface                 idioms

Model View - View Model

View/View Controller

View Model

Model

owns

updates

updates

owns

✅ VCs do not know anything about or how to fetch the model

✅ VCs do not mutate the model directly, but through the View Model

✅ VCs bind on the View Model's state (data binding)

Model View - View Model

Data binding

View Controllers need a way of knowing when something in their View Model changes so they can update the UI.

✅ Delegation

✅ RxSwift & the rest...

✅ Closures

✅ A simple generic object....

Model View - View Model

Generic for Data Binding

class Observable<T> {
    
    typealias Listener = (T) -> Void
    var listener: Listener?
    
    func bind(_ listener: Listener?) {
        self.listener = listener
    }
    
    func bindAndFire(_ listener: Listener?) {
        self.listener = listener
        listener?(value)
    }
    
    var value: T {
        didSet {
            listener?(value)
        }
    }
    
    init(_ v: T) {
        value = v
    }
}

Services & DI

Singletons are an ANTI-PATTERN. Singletons must DIE.

"Use a singleton when you need the ☀️, not a 🔦"

  • Generally used as a global instance, which is bad because you hide the dependencies of your code.
  • Violate the Single Responsibility Principle: They control their own creation and lifecycle
  • They cause your code to be tightly coupled. Mocking them under test is impossible by theory.
  • They carry state in their entire lifecycle which violates FIRST principles of testing (tests should be isolated)

Services & DI

Instead isolate your dependencies into separate classes or structs and pass them (inject them) where necessary.

public class Services: HasPushNotificationsService, HasAPIService {
        
    /// A service for interacting with the server API
    let apiService: APIService
     
    /// A service for managing push notifications related concepts
    let pushNotificationsManager: PushNotificationsManager!
    
    
    init(apiService: APIService,
         pushNotificationsManager: PushNotificationsManager,
    {
        
        self.apiService = apiService
        self.pushNotificationsManager = pushNotificationsManager
      
    }

}

Services & DI

Be explicit about it!

protocol HasAPIService {
    var apiService: APIService { get }
}

class ViewControllerViewModel {

    typealias Dependencies = HasAPIService & HasPushNotificationsService
    private var dependencies: Dependencies!

    inject(dependencies: Dependencies) {
        self.dependencies = dependencies  
    }

}

How it all fits together

✅ Initialise an object to hold your dependencies/services

✅ In appDidFinishLaunching, create the App Coordinator and             inject the dependencies/services to it.

✅ The App Coordinator has the logic on which coordinator takes       the ball next. 

✅ Each coordinator is responsible for creating and configuring           each View Model for each View Controller it presents

✅ Each coordinator is responsible for initialising, configuring,             presenting dismissing its View Controllers

✅ View Controllers take user input and ask their coordinator to           take the lead (via delegation)

✅ View Controllers are passive. They only present/update data           based on what VM tells them to do. They DO NOT handle                 navigation or mutate their models directly.

Questions?

@attheodo

http://attheo.do

Made with Slides.com