#if canImport(UIKit) #if !os(watchOS) import UIKit /** A `Coordinator` managing and sending events related to actions by the user. Coordinators act as a layer on top of view controllers, and manage preparing data for the view controllers (reaching out to any services), as well as reacting to events generated by the view controllers. In an MVC context, Coordinators essentially act as the ViewController of the Model layer, but specific for a given screen (in the Mobile context). E.G. you wouldn't have a "DatadaseCoordinator", though if you were writing a SQL browser, you might have a "TableCoordinator" or a "RowCoordinator". Coordinators should be initialized with everything they need to start work, and then actually start that work (invoke services, subscribe to events, present alerts if need be) in ``-start()`` */ @MainActor public protocol UICoordinator: AnyObject { /// The child coordinators of this coordinator. var children: [UICoordinator] { get } /// Whether this coordinator is active or not. var isActive: Bool { get } /// The singular view controller this Coordinator coordinates. var rootViewController: UIViewController { get } /** Adds the given coordinator as a child of the receiver. - Parameter coordinator: The Coordinator to make a child of the receiving Coordinator. */ func addChild(_ coordinator: UICoordinator) /** Removes the given coordinator as a child of the receiver. - Parameter coordinator: The Coordinator to remove. */ func removeChild(_ coordinator: UICoordinator) /** Starts work for this coordinator. This is analogous to `-viewDidLoad()` in `UIViewController`. Instead of putting any logic in `init`, logic should be set up and performed in `start()`. */ func start() async /** Stops work for this coordinator. Implementers should prepare for this coordinator to be de-initialized, and cancel any outstanding publishers. - Note: It's possible for `-stop()` to be called, and then ``-start()`` to later be called again. This is completely valid. */ func stop() async } extension UICoordinator { /** Combines the work of adding a child coordinator and calling ``-start()`` on it. - Parameter child: The coordinator to start and add as a child to the receiver. If the coordinator is already a child of the receiver, then this method no-ops. - Note: This method first adds the coordinator as a child, then calls ``-start()`` on it. */ @MainActor public func pushAndStart(child: UICoordinator) async { guard children.contains(where: { $0 === child }) == false else { return } addChild(child) await child.start() } /** Combines the work of removing a child coordinator and calling ``-stop()`` on it. - Parameter child: The coordinator to stop and remove as a child of the receiver. If the "child" is not actually a child of the receiver, then this mthod no-ops. - Note: This method first calls ``-stop()`` on the child coordinator, then removes it as a child of the receiver. */ @MainActor public func stopAndPop(child: UICoordinator) async { guard children.contains(where: { $0 === child }) else { return } await child.stop() removeChild(child) } } /// A concrete implementation of Coordinator, using NSObject as the superclass (so that it, and all subclasses, can easily conform to delegates requiring `NSObjectProtocol`). @MainActor open class BaseUICoordinator: NSObject, UICoordinator { /// The list of child coordinators public private(set) var children: [UICoordinator] = [] /// Whether the coordinator is active or not. public private(set) var isActive: Bool = false /// The root view controller of the coordinator. public let rootViewController: UIViewController /** Initializes the coordinator. - Parameter rootViewController: The `UIViewController` to set as the root view controller. - Note: BaseCoordinator doesn't actually do anything with the `rootViewController` except have a reference to it. */ public init(rootViewController: UIViewController) { self.rootViewController = rootViewController super.init() } /** Adds the given coordinator as a child of the receiver. - Parameter coordinator: The Coordinator to make a child of the receiving Coordinator. */ public func addChild(_ coordinator: UICoordinator) { children.append(coordinator) } /** Removes the given coordinator as a child of the receiver. - Parameter coordinator: The Coordinator to remove. */ public func removeChild(_ coordinator: UICoordinator) { children.removeAll(where: { $0 === coordinator }) } /** Starts work for the coordinator. Your `BaseCoordinator` subclasses should implement this method and super-up as one of the first things it does. - Note: This sets ``isActive`` to true, until ``-stop()`` is called. */ open func start() async { isActive = true } /** Starts work for the coordinator. Your `BaseCoordinator` subclasses should implement this method and super-up as one of the last things it does. - Note: This sets ``isActive`` to false, until ``-start()`` is called again. */ open func stop() async { isActive = false } } /** A concrete implementation of a `Coordinator` for a navigation hierarchy. */ @MainActor public class NavigationUICoordinator: BaseUICoordinator { /// The `UINavigationController` for this hierarchy. /// - Warning: If you set the `navigationController`'s delegate yourself, please be sure to override `navigationController(_:animationControllerFor:from:to:)` and forward the call to this object. You can safely ignore the return value for `navigationController(_:animationControllerFor:from:to:)` from this object (it's always `nil`). public let navigationController: UINavigationController /** Creates a new NavigationCoordinator for a new navigation hierarchy. - Returns: A NavigationCoordinator for an empty hierarchy. */ public static func root() -> NavigationUICoordinator { return NavigationUICoordinator(navigationController: UINavigationController()) } /** Creates a new NavigationCoordinator for this hierarchy. - Parameter navigationController: The navigationController to use for the hierarchy. */ internal init(navigationController: UINavigationController) { self.navigationController = navigationController super.init(rootViewController: navigationController) navigationController.delegate = self } /** Pushes the coordinator onto the navigation hierarchy. Also pushes the coordinator's `rootViewController` onto the `navigationController` navigation hierarchy. - Parameter coordinator: The child coordinator to push onto the navigation hierarchy. - Parameter animated: Whether to tell the navigation controller to animate pushing the child coordinator's root view controller. - ToDo: It would be nice to expose something like `push` on ``BaseCoordinator`` subclasses that correctly pushes onto the navigation stack when you call `addChild` on that coordinator. */ public func push(coordinator: UICoordinator, animated: Bool = true) async { guard children.contains(where: { $0 === coordinator }) == false else { return } await pushAndStart(child: coordinator) navigationController.pushViewController(coordinator.rootViewController, animated: true) } /** Pops the last child coordinator from the navigation hierarchy. Also pop's that child's root view controller, under the assumption that the child's rootViewController is the navigationController's topViewController. - Warning: This may remove the wrong view controller (or stop the wrong coordinator) if the last child coordinator's `rootViewController` is not the same as the navigationController's `topViewController`. This should only be the case if you manually use the `navigationController` or the other coordinator management methods. - ToDo: It would be nice to expose something like `pop` on ``BaseCoordinator`` such that you don't need to pass instances of `NavigationCoordinator` around to pop (or push) coordinators beyond the top-level. */ public func pop(animated: Bool = true) async { guard let coordinator = children.last else { return } navigationController.popViewController(animated: animated) await stopAndPop(child: coordinator) } } extension NavigationUICoordinator: UINavigationControllerDelegate { /** This tracks when the user uses the UI to pop view controllers, finds the relevant coordinator for that view controller, stops it, and removes it from the child coordinators. */ @MainActor public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { guard operation == .pop else { return nil } if let coordinator = children.first(where: { $0.rootViewController == fromVC }) { Task { @MainActor in await stopAndPop(child: coordinator) } } return nil } } #endif #endif