UINavigationController
that owns it, “communicating” with their own “brothers” controllers (even initializing them and passing to the navigation stack) - in general, do a lot of things than they are not supposed to even suspect.UIApplicationDelegate
).UINavigationController
, the click on which is not handled by our code, and we can only get a callback .UINavigationController
. He can do everything with UIViewController
instances with respect to navigation - “push” / “pop”, “present” / “dismiss”, plus manipulations with the “root” controller . An example of the interface of such a router: import UIKit protocol Router { func present(_ module: UIViewController, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) func push(_ module: UIViewController, animated: Bool, completion: (() -> Void)?) func popModule(animated: Bool) func setAsRoot(_ module: UIViewController) func popToRootModule(animated: Bool) }
UINavigationController
instance and does not contain anything particularly tricky in itself. The only restriction is that other instances of UINavigationController
cannot be passed as interface argument values (for obvious reasons: UINavigationController
cannot contain UINavigationController
in its stack — this is a UIKit
constraint). class Coordinator { private var childCoordinators = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } }
UIViewController
. To ensure the interaction of the router and coordinators, we introduced the following interface: protocol Presentable { func presented() -> UIViewController }
Coordinator
and implement the Presentable
interface, and the router interface should take the following form: protocol Router { func present(_ module: Presentable, animated: Bool) func dismissModule(animated: Bool, completion: (() -> Void)?) func push(_ module: Presentable, animated: Bool, completion: (() -> Void)?) func popModule(animated: Bool) func setAsRoot(_ module: Presentable) func popToRootModule(animated: Bool) }
Presentable
approach also allows coordinators to be used inside modules that are written to interact directly with UIViewController
instances without exposing them (modules) to cardinal processing.) final class FirstCoordinator: Coordinator, Presentable { func presented() -> UIViewController { return UIViewController() } } final class SecondCoordinator: Coordinator, Presentable { func presented() -> UIViewController { return UIViewController() } } let nc = UINavigationController() let router = RouterImpl(navigationController: nc) // Router implementation. router.setAsRoot(FirstCoordinator()) router.push(SecondCoordinator(), animated: true, completion: nil) router.popToRootModule(animated: true)
start()
method - a version that captivated initially with its simplicity and brevity.Coordinator
mentioned above will obviously not be redundant. But to the common interface, you need to add the same method: protocol Coordinator { func add(dependency coordinator: Coordinator) func remove(dependency coordinator: Coordinator) func start() } class BaseCoordinator: Coordinator { private var childCoordinators = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } func start() { } }
start()
method can be either left with an empty implementation or shoved there is something like fatalError(_:file:line:)
(forcing the method to override this method by successors). Personally, I prefer the first option. extension Coordinator { func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } }
final class SomeCoordinator: BaseCoordinator { override func start() { // ... } }
UINavigationController
.UIViewController
, the coordinator can, for example, accept a new instance of UINavigationController
with an empty stack.UINavigationController
on to other coordinators, which it spawns. And they can also do with the current state of navigation what they need: “push”, “present”, and at least replace the entire navigation stack. protocol CoordinatorDependencies { func add(dependency coordinator: Coordinator) func remove(dependency coordinator: Coordinator) } final class DefaultCoordinatorDependencies: CoordinatorDependencies { private let dependencies = [Coordinator]() func add(dependency coordinator: Coordinator) { // ... } func remove(dependency coordinator: Coordinator) { // ... } } final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies init(dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { dependencies = dependenciesManager } func start() { // ... } }
UIViewController
: final class SomeViewController: UIViewController { }
final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager } func start() { let vc = SomeViewController() navigationController?.pushViewController(vc, animated: true) } }
protocol SomeViewControllerRoute: class { func onSomeEvent() } final class SomeViewController: UIViewController { private weak var route: SomeViewControllerRoute? init(route: SomeViewControllerRoute) { self.route = route super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @IBAction private func buttonAction() { route?.onSomeEvent() } } final class SomeCoordinator: Coordinator { private let dependencies: CoordinatorDependencies private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager } func start() { let vc = SomeViewController(route: self) navigationController?.pushViewController(vc, animated: true) } } extension SomeCoordinator: SomeViewControllerRoute { func onSomeEvent() { // ... } }
UINavigationController
instance passed to him and tracks the event of interest to us.UINavigationController
can only be the successor of NSObject
.start()
call, adds a UIViewController
to the UINavigationController
stack. By clicking on the back button on the UINavigationBar
all that needs to be done is to let the originating coordinator know that the generated coordinator has finished its work (flow). To do this, we have introduced another delegation tool: each delegate is assigned a delegate whose interface is implemented by the originating coordinator: protocol CoordinatorFlowListener: class { func onFlowFinished(coordinator: Coordinator) } final class MainCoordinator: NSObject, Coordinator { private let dependencies: CoordinatorDependencies private let navigationController: UINavigationController init(navigationController: UINavigationController, dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) { self.navigationController = navigationController dependencies = dependenciesManager super.init() } func start() { let someCoordinator = SomeCoordinator(navigationController: navigationController, flowListener: self) dependencies.add(someCoordinator) someCoordinator.start() } } extension MainCoordinator: CoordinatorFlowListener { func onFlowFinished(coordinator: Coordinator) { dependencies.remove(coordinator) // ... } } final class SomeCoordinator: NSObject, Coordinator { private weak var flowListener: CoordinatorFlowListener? private weak var navigationController: UINavigationController? init(navigationController: UINavigationController, flowListener: CoordinatorFlowListener) { self.navigationController = navigationController self.flowListener = flowListener } func start() { // ... } } extension SomeCoordinator: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { guard let fromVC = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return } if navigationController.viewControllers.contains(fromVC) { return } if fromVC is SomeViewController { flowListener?.onFlowFinished(coordinator: self) } } }
MainCoordinator
does nothing: it simply launches the “flow” of another coordinator - in real life, of course, it is useless. In our application, the MainCoordinator
receives data from outside, according to which it determines the state of the application - authorized, unauthorized, etc. - and what kind of screen you need to show. Depending on this, it launches the flow of the corresponding coordinator. If the child coordinator has finished his work, the main coordinator receives a signal about this through the CoordinatorFlowListener
and, say, starts the flow of another coordinator.UINavigationController
delegate method to each coordinator with a nearly identical implementation. (The first approach lacks this disadvantage, but instead more generously shares its internal knowledge of the appointment of a specific coordinator.)start()
method or by calling onFlowFinished(coordinator:)
. And anything can happen in these places, and this will always be “hardcoded” behavior: adding a controller to the stack, replacing the stack, returning to the root controller - whatever. And it all depends not on the competencies of the current controller, but on the external conditions.Thank you for reading this place! Hope to learn something useful for yourself. And if you suddenly want "more than me", then here is a link to my Twitter .
Source: https://habr.com/ru/post/444038/
All Articles