swift mvvmc

Coordinators with MVVM

Harshal 2020/02/19 11:58:08
3688

MVVM-C(Model View ViewModel- Coordinator) has become some sort of standard as an architecture for iOS apps. It offers a good separation of concerns, a good way to format data and great view binding mechanisms.

MVVM Overview

Model: Model is pure data, which is interpreted through ViewModel. It has no arithmetic logic or is dependent on other components.

View: View (generally refers to UIKit related categories, such as UIView or UIViewController) only focuses on presenting data obtained from the ViewModel. Once the data is updated (for example, the following code is updated by setting a new ViewModel), the UI will be updated accordingly.

ViewModel: Calculating what to display and when to display it is now contained within the view model. In true MVVM form, our view controller doesn't even have direct access to the models it displays!

Coordinator

This is a pattern introduce by Soroush Khanlou. Mostly view controllers become so coupled and dependent on each other. And your navigation is scattered all over your code. The coordinator pattern provides an encapsulation of app navigation logic.

Using the coordinator pattern in iOS apps lets us remove the job of app navigation from our view controllers, helping make them more manageable and more reusable, while also letting us adjust our app's flow whenever we need.

Simply, all screen's navigation will be managed by coordinators.

Let's start the implementation, first download project implemented in MVVM and default navigation here

 

In this project, navigate to photo detail screen and view photo implemented in UICollectionView didSelectItemAt method

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let photoDetailViewController = storyboard.instantiateViewController(withIdentifier: "PhotoDetailViewController") as! PhotoDetailViewController
        photoDetailViewController.photoURL = viewModel.getCellViewModel( at: indexPath.row).photoURL
        self.navigationController?.pushViewController(photoDetailViewController, animated: true)
    }

 

So PhotoListViewController has to responsible for managing navigation to PhotoDetailViewController and it creates extra overhead, breaks the SOLID principles and difficult to cover test coverage too.

 


Existing flow

Coordinator Flow

 

To de-couple this navigation, let's create Coordinator protocol:

protocol Coordinator {
    func start()
}

 

Create Storyboarded protocol with a default implementation in extension to instantiate view controller from user base storyboard.

Then confirm this to PhotoListCoordinator and PhotoDetailViewController 

protocol Storyboarded {
   static func instantiate() -> Self
}

extension Storyboarded where Self: UIViewController {
    
    static func instantiate() -> Self {
        let fullName = NSStringFromClass(self)
        let className = fullName.components(separatedBy: ".")[1]
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
        return storyboard.instantiateViewController(withIdentifier: className) as! Self
    }
}

 

First, create PhotoDetailCoordinator for PhotoDetailViewController sd which accepts photo URL and presenter to push screen on the navigation stack.

Implement start() of coordinator with assigning photo URL set in init method.

final class PhotoDetailCoordinator: Coordinator {
    
    private var presenter: UINavigationController
    private var photoDetailViewController: PhotoDetailViewController?
    private let photoURL: String
    
    init(presenter: UINavigationController, photoURL: String) {
        self.presenter = presenter
        self.photoURL = photoURL
    }
    
    func start() {
        
        let photoDetailVC = PhotoDetailViewController.instantiate()
        photoDetailVC.photoURL = photoURL
        self.photoDetailViewController = photoDetailVC
        
        presenter.pushViewController(photoDetailVC, animated: true)
    }
}

 

Now create PhotoListCoordinator which confirms to Coordinator

Also add UINavigationController, PhotoListViewController properties. Init presenter with UINavigationController and write navigation login in start() method

final class PhotoListCoordinator: Coordinator {
    
    private var presenter: UINavigationController
    private var photoListViewController: PhotoListViewController?
    
    init(presenter: UINavigationController) {
        self.presenter = presenter
    }
    
    func start() {
        let photoListViewController = PhotoListViewController.instantiate()
        photoListViewController.photoListVCDelegate = self
        self.photoListViewController = photoListViewController
        presenter.pushViewController(photoListViewController, animated: true)
    }
    
}

 

We need to create a delegate PhotoListVCDelegate to communicate Coordinator with PhotoListViewController

protocol PhotoListVCDelegate: class {
    func photoListVC(_ controller: PhotoListViewController, didSelect photo: String)
}

 

also, in PhotoListViewController extension, replace didSelectItemAt method with following

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.photoListVCDelegate?.photoListVC(self, didSelect: viewModel.getCellViewModel( at: indexPath.row).photoURL)
    }

here invoke the delegate method and call the photoListVC method by passing the selected view model photo URL.

 

Now we need entry point and manager for these coordinator i.e ApplicationCoordinator which will have a window and root initializer for the Coordinator.

Initialize  PhotoListCoordinator, window, and refactor showing root view screen from AppDelegate to start() method.

final class ApplicationCoordinator: Coordinator {
    
    private let window: UIWindow
    private let rootViewController: UINavigationController
    private var photoListCoordinator: PhotoListCoordinator?
    
    init(window: UIWindow) {
        self.window = window
        rootViewController = UINavigationController()
        rootViewController.navigationBar.prefersLargeTitles = true
        rootViewController.navigationBar.barStyle = .black
        photoListCoordinator = PhotoListCoordinator(presenter: rootViewController)
    }
    
    func start() {
        window.rootViewController = rootViewController
        photoListCoordinator?.start()
        window.makeKeyAndVisible()
    }
    
}

 

Add UIWindow and ApplicationCoordinator properties and update the didFinishLaunchingWithOptions method with below initialization

var window: UIWindow?
    var applicationCoordinator: ApplicationCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        let window = UIWindow(frame: UIScreen.main.bounds)
        let appCordinator = ApplicationCoordinator(window: window)
        self.window = window
        self.applicationCoordinator = appCordinator

        appCordinator.start()

        return true
    }

Last but not least, remove storyboard reference from the Main Interface option under target General tab

 

Done :)

The final project can be download from here

 

References

https://www.appcoda.com.tw/coordinator/

https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps

 

Cheers !!!

Harshal