To run the example project, clone the repo, and run pod install
from the Example directory first.
ADCoordinator is written in Swift 5.1. Compatible with iOS 10.0+.
ADCoordinator is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'ADCoordinator'
Each coordinator is instantiated with a graphical context it does not own. For instance a coordinator can be initialized with a UIWindow
or UINavigationController
.
class ApplicationCoordinator: Coordinator {
private unowned var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
}
Note: the unowed
here is mandatory to avoid retaining the UIKit object.
The coordinator is then responsible to create and display view controllers inside this graphical context.
As a convention, we use a method named start
to tell the coordinator to create its first view controller:
class ApplicationCoordinator: Coordinator, MainViewControllerDelegate {
...
// MARK: - Public
func start() {
let viewController = MainViewController()
// the view controller can notify the coordinator when it needs to navigate
viewController.delegate = self
// display the first view controller in the navigation controller
navigationController.pushViewController(viewController, animated: false)
// Automatic memory management
bindToLifecycle(of: viewController)
}
}
The outside world is responsible to create and display the graphical context on screen.
In this case the AppDelegate
will create a navigationController
and set it to its window rootViewController
.
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var applicationCoordinator: ApplicationCoordinator?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window
// Create the graphical context
let navigationController = UINavigationController()
// Create the coordinator with the graphical context
applicationCoordinator = ApplicationCoordinator(navigationController: navigationController)
// Let the coordinator display its content
applicationCoordinator?.start()
// Present the graphical context on screen
window.rootViewController = navigationController
window.makeKeyAndVisible()
return true
}
}
Once we want to change the view controller on screen, we ask the coordinator. No navigation is allowed outside the coordinator.
Either we push a view controller horizontally (meaning on the same navigation controller stack) or present a view controller vertically (meaning on another navigation controller that comes on top).
When the coordinator receives a callback from the view controller on screen to navigate, the simplest way is just to create a new view controller and push it onto the navigation stack.
class ApplicationCoordinator: Coordinator, MainViewControllerDelegate {
...
// MARK: - MainViewControllerDelegate
func mainViewControllerDidRequestPush() {
let detailViewController = DetailViewController()
navigationController.pushViewController(detailViewController, animated: true)
}
}
In this case it means creating a new graphical context (most of the time a navigation controller) and displaying it on top.
Rule of thumbs: create a new coordinator each time the method UIViewController.present(_:animated:completion)
is called.
class DetailCoordinator: Coordinator {
private unowned var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
// MARK: - Public
func start() {
let viewController = DetailViewController()
navigationController.pushViewController(viewController, animated: false)
// Automatic memory management
bindToLifecycle(of: viewController)
}
}
This is always the same idea, the coordinator is given a graphical context it does not own and perform actions inside it. This is the parent coordinator that creates the graphical context and the child coordinator.
class ApplicationCoordinator: Coordinator, MainViewControllerDelegate {
...
// MARK: - MainViewControllerDelegate
func mainViewControllerDidRequestPresent() {
// The new graphical context for the child coordinator
let navigationController = UINavigationController()
// Create the child coordinator
let coordinator = DetailCoordinator(navigationController: navigationController)
// Add child to the tree of coordinators
addChild(coordinator)
// Start the coordinator
coordinator.start()
// Present the new graphical context on screen
self.navigationController.present(navigationController, animated: true)
}
}
Each time a new coordinator is created, it has to be retained by another object. The root coordinator is retained by the AppDelegate
and each coordinator stores an array of children coordinators.
A coordinator can retain a child coordinator calling the addChild
method and can remove a child from its children
array with the removeChild
method.
class Coordinator {
private(set) var children: [Coordinator] = []
private(set) weak var parent: Coordinator?
func addChild(_ coordinator: Coordinator) { ... }
func removeChild(_ coordinator: Coordinator) { ... }
}
Since iOS 13, a new modal presentation has been introduced. The presentation does not cover the entire screen, and more importantly, the user can dismiss the modal swipping down, without any callback to the view controller.
We saw earlier that each modal presented leads to the creation of a new coordinator. Once the modal is dismissed, the associated coordinator should be released. If the user taps a button, an action is associated to this button, and the view controller can forward the intent to the right coordinator to call removeChild
. But if the user swipes down a modal on iOS 13, no action is triggered.
To overcome this issue, each coordinator should call bindToLifecyle(of:)
passing the view controller it is bound to as parameter.
class DetailCoordinator: Coordinator {
...
// MARK: - Public
func start() {
let viewController = DetailViewController()
navigationController.pushViewController(viewController, animated: false)
bindToLifecycle(of: viewController)
}
}
This method will then observe the lifecycle of viewController
, and once it has been deallocated, will ask the parent coordinator to automatically call removeChild
. This way the child coordinator will be deallocated at the same time its graphical context does.
Note: the same pattern can be applied in an horizontal navigation, when you want to extract duplicated code into a coordinator. When the user hits the pop button in a navigation bar, no action is triggered in your app. So you have to use bindToLifecycle(of:)
with the view controller you want to observe the pop.
Choosing the right object to pass to the bindToLifecycle(of:)
is the key to good memory management.
If a coordinator is initialized with a navigation controller, you should observe the first view controller the coordinator pushes on the stack. That way:
- when the navigation controller is released, so does the view controller, and the coordinator is notified
- if the view controller was not the first in the stack and the user pops, then the view controller is released, and once again, the coordinator is notified
- if you want to change the navigation controller stack, you can call
bindToLifecycle(of:)
with another view controller, to remove the previous observer and create a new one
ADCoordinator is available under the MIT license. See the LICENSE file for more info.