Coordinators with MVVM
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 !!!