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.
Usually in ViewController, we navigate screen with segue.
Because of segue, 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
.
From this image, 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.
Example Code
I uploaded full code on Github Repository.
When you see the code folder, there are 5 scripts.
-
Coordinator: Protocol which abstract Coordinators
-
Main Coordinator: Parent Coordinator which can include initial VC, child coordinators, and method which present other VCs.
-
Storyboarded: If the project uses storyboard, protocol is supported for each VC.
-
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: AnyObject {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set}
func start()
}
Main Coordinator
Recieve a navigation controller from app delegate or parent coordinator.
Additionally, MainCoordinator can make VC instance and show other VC on screen by present()
.
UINavigatorController is set by AppDelegate or parent coordinator.
We knows from present()
, current coordinator and VC are coupled until
presenting new VC.
When you initiate VC, you should set the storyboard name, and then storyboarded protocol would be excuted and help to initiate by own extension.
class MainCoordinator: Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let view = SplashView.instantiate(storyboardName: "Splash")
view.coordinator = self
navigationController.pushViewController(view, animated: false)
}
func presentSigninView() {
let view = SigninView.instantiate(storyboardName: "Login")
view.coordinator = self
navigationController.pushViewController(view, animated: false)
}
}
Storyboarded
By adding protocol for each VC, it automatically supports initializing storyboard.
However, you should send parameter of storyboard name as I mentioned.
For recognizing IBOutlet from storyboard, you should customize UIViewController as well.
This example didn’t reflect it.
protocol Storyboarded {
static func instantiate(storyboardName: String) -> Self
}
extension Storyboarded where Self: UIViewController {
static func instantiate(storyboardName: String) -> Self {
let id = String(describing: self)
let storyboard = UIStoryboard(name: storyboardName, bundle: Bundle.main)
return storyboard.instantiateViewController(identifier: id) as! Self
}
}
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()
navController.navigationBar.isHidden = true
coordinator = MainCoordinator(navigationController: navController)
coordinator?.start()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = navController
window?.makeKeyAndVisible()
return true
}
}
ViewController
Because rootViewcontroller is controlled by coordinator, you just reference the coordinator and excutes present()
method what you want.
class SplashView: UIViewController, Storyboarded {
weak var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func goToSignin(_ sender: Any) {
coordinator?.presentSigninView()
}
}
Conclusion
Coordinator supports initiation of VC, so it has many things to set first like storyboard, ViewModel, and VC Type.
In my opinion, it is the reason why using programmatic UI is more comfortable than using storyboard for coordinator pattern.
For making it programmically, I will arrange about those things.
I hope it helps you.