on
Coordinator Pattern in Swift
Why do we need to change?
Recently, I realized that my app’s UI Layer instances are too much coupled.
After I searched why my code was not clean, I realized there are two reasons.
-
ViewModel instance is generated by ViewController.
-
All the viewcontroller navigation codes are in viewontroller.
Let’s start by looking at code most iOS developers have written a hundred or more times:
if let vc = storyboard?.instantiateViewController(withIdentifier: "SomeVC") {
navigationController?.pushViewController(vc, animated: true)
}
All ViewControllers have dependencies each other. Additionally, ViewModel also depend on their own controller. It makes developer to manage code hard later.
Therefore, I decided to remove all the navigating segues from viewcontrollers.
Because of initializing viewcontroller from viewcontroller, I should set the viewmodel in viewcontroller as well.
I need a Hub which manages navigation and Dependency injection.
It is Coordinator. Coordinator receives the request which ask navigating from current VC to the next VC. As a result, coordinator initiate the instance of VC, and show the screen.
Each VCs do not need to reference other VC. Furthermore, coordinator also supports dependency injection between ViewController and ViewModel.
As a result, All the coupled instance is seperated. Each instance classes do not need to manage other instances.
Coordinators in action
-
Coordinator
: Protocol which abstract Coordinators -
Storyboarded
: If the project uses storyboard, protocol is supported for each VC. -
Main Coordinator
: Parent Coordinator which can include initial VC, child coordinators, and method which present other VCs. -
AppDelegate
: Appdelegate which includes main coordinator. -
ViewController
: Coordinator method which replaced from segue can be excuted on VC.
Coordinator Protocol
Protocol which abstracts implementation of coordinator. start()
is neccessary for initial VC with storyboard.
Each coordinator can have children by the list of coordinator.
protocol Coordinator {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
Storyboarded
By adding protocol for each VC, it automatically supports initializing storyboard.
protocol Storyboarded {
static func instantiate() -> Self
}
extension Storyboarded where Self: UIViewController {
static func instantiate() -> Self {
let className = String(describing: self)
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
return storyboard.instantiateViewController(identifier: className) as! Self
}
}
Main Coordinator
It’s a class rather than a struct because this coordinator will be shared across many view controllers.
It has an empty childCoordinators
array to satisfy the requirement in the Coordinator
protocol, but we won’t be using that here.
It also has a navigationController
property as required by Coordinator
, along with an initializer to set that property.
The start()
method is the main part: it uses our instantiate()
method to create an instance of our ViewController
class, then pushes it onto the navigation controller.
class MainCoordinator: Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = ViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
func buySubscription() {
let vc = BuyViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: true)
}
func createAccount() {
let vc = CreateAccountViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: true)
}
}
AppDelegate
Set the rootViewController as NavigationController which can be controlled by coordinator.
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var coordinator: MainCoordinator?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let navController = UINavigationController()
coordinator = MainCoordinator(navigationController: navController)
coordinator?.start()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = navController
window?.makeKeyAndVisible()
return true
}
}
ViewController
class ViewController: UIViewController, Storyboarded {
weak var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func buyTapped(_ sender: Any) {
coordinator?.buySubscription()
}
@IBAction func createAccount(_ sender: Any) {
coordinator?.createAccount()
}
}
Conclusion
I hope this has given you a useful introduction to the power of coordinators:
-
No view controllers know what comes next in the chain or how to configure it.
-
Any view controller can trigger your purchase flow without knowing how it’s done or repeating code.
-
You can add centralized code to handle iPads and other layout variations, or to do A/B testing.
-
But most importantly, you get true view controller isolation: each view controller is responsible only for itself.
References
https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps
https://www.hackingwithswift.com/articles/175/advanced-coordinator-pattern-tutorial-ios