├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Coordinator │ ├── Coordinating.swift │ ├── Coordinator.swift │ ├── NavigationCoordinator.swift │ └── UIKit-CoordinatingExtensions.swift └── documentation ├── Class.md ├── Implement.md ├── Library.md └── Pattern.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2016 Aleksandar Vacić, Radiant Tap 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Coordinator", 8 | platforms: [ 9 | .iOS(.v15), 10 | .tvOS(.v15), 11 | .visionOS(.v1) 12 | ], 13 | products: [ 14 | .library( 15 | name: "Coordinator", 16 | targets: ["Coordinator"] 17 | ), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "Coordinator" 22 | ) 23 | ], 24 | swiftLanguageModes: [.v6] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/github/tag/radianttap/Coordinator.svg?label=current)](https://github.com/radianttap/Coordinator/releases) 2 | [![](https://img.shields.io/github/license/radianttap/Coordinator.svg)](https://github.com/radianttap/Coordinator/blob/master/LICENSE) 3 | ![](https://img.shields.io/badge/swift-6.0-223344.svg?logo=swift&labelColor=FA7343&logoColor=white) 4 | \ 5 | ![platforms: iOS|tvOS|watchOS|macOS|visionOS](https://img.shields.io/badge/platform-iOS_15_·_tvOS_15_·_visionOS_1-blue.svg) 6 | 7 | # Coordinator 8 | 9 | Implementation of _Coordinator_ design pattern. It is *the* application architecture pattern for iOS, carefully designed to fit into UIKit; so much it could easily become `UICoordinator`. 10 | 11 | Since this is *core architectural pattern*, it’s not possible to explain its usage with one or two clever lines of code. Give it a day or two; analyze and play around. I’m pretty sure you’ll find it worthy of your time and future projects. 12 | 13 | ## Installation 14 | 15 | - version 8.x is using Swift 6 language mode and has strict concurrency turned ON 16 | - version 7.x and up is made with Swift 5.5 concurrency in mind (async / await) 17 | - versions before that (6.x) use closures 18 | 19 | Just drag `Coordinator` folder into your project — it‘s only a handful of files. 20 | 21 | Or add add this repo’s URL through Swift Package Manager. 22 | 23 | ## Documentation 24 | 25 | The _why_ and _how_ and... 26 | 27 | - the [Pattern](documentation/Pattern.md) 28 | - the [Library](documentation/Library.md) 29 | - the [Class](documentation/Class.md) 30 | - recommended [Implementation](documentation/Implement.md) 31 | 32 | ## License 33 | 34 | [MIT](https://choosealicense.com/licenses/mit/), as usual. 35 | 36 | ## Give back 37 | 38 | If you found this code useful, please consider [buying me a coffee](https://www.buymeacoffee.com/radianttap) or two. ☕️😋 39 | -------------------------------------------------------------------------------- /Sources/Coordinator/Coordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinating.swift 3 | // Radiant Tap Essentials 4 | // 5 | // Copyright © 2017 Radiant Tap 6 | // MIT License · http://choosealicense.com/licenses/mit/ 7 | // 8 | 9 | import UIKit 10 | 11 | /// Protocol to define what is required for an object to be Coordinator. 12 | /// 13 | /// It also simplifies coordinator hierarchy management. 14 | @MainActor 15 | public protocol Coordinating: AnyObject { 16 | /// Unique string to identify specific Coordinator instance. 17 | /// 18 | /// By default it will be String representation of the Coordinator's subclass. 19 | /// If you directly instantiate `Coordinator`, then you need to set it manually. 20 | var identifier: String { get } 21 | 22 | /// Parent Coordinator can be any other Coordinator. 23 | var parent: Coordinating? { get set } 24 | 25 | /// A dictionary of child Coordinators, where key is Coordinator's identifier property. 26 | var childCoordinators: [String: Coordinating] { get } 27 | 28 | /// Returns either `parent` coordinator or `nil` if there isn‘t one 29 | var coordinatingResponder: UIResponder? { get } 30 | 31 | /// Tells the coordinator to start, which means at the end of this method it should 32 | /// display some UIViewController. 33 | func start() 34 | 35 | /// Tells the coordinator to stop, which means it should clear out any internal stuff 36 | /// it possibly tracks. 37 | /// I.e. list of shown `UIViewController`s. 38 | func stop() 39 | 40 | /// Essentially, this means that Coordinator requests from its parent to stop it. 41 | /// 42 | /// Useful in cases where a particular Coordinator instance know that at particular 43 | /// moment none of its UIVCs will be visible or useful anymore. 44 | /// This is a chance for parentCoordinator to nicely transitions to some other Coordinator. 45 | func coordinatorDidFinish(_ coordinator: Coordinating) 46 | 47 | /// Adds the supplied coordinator into its `childCoordinators` dictionary and calls its `start` method 48 | func startChild(coordinator: Coordinating) 49 | 50 | /// Calls `stop` on the supplied coordinator and removes it from its `childCoordinators` dictionary 51 | func stopChild(coordinator: Coordinating) 52 | 53 | /// Activate Coordinator which was used before. 54 | /// 55 | /// At the least, this Coordinator should assign itself as `parentCoordinator` of its `rootViewController`, 56 | /// ready to start displaying its content View Controllers. This is required due to the possibility that 57 | /// multiple Coordinator can share one UIViewController as their root VC. 58 | /// 59 | /// See NavigationCoordinator for one possible usage. 60 | func activate() 61 | 62 | /// Coordinator will take ownership over root UIVC. 63 | /// 64 | /// This should call `activate()` first and then *replace* 65 | /// 66 | /// See NavigationCoordinator for one possible usage. 67 | func takeover() 68 | } 69 | 70 | // MARK: - Dictionary Extension 71 | 72 | /// Sometimes a coordinator needs to make decisions base on the information whether or 73 | /// not a certain child coordinator is already active. In this case the coordinator has to 74 | /// consult its active child coordinators. Finding out whether a child coordinator of a certain 75 | /// type is already in the dictionary of child coordinators can be made a bit more elegant by 76 | /// means of the following dictionary extension. 77 | /// 78 | /// So one can call e.g. 79 | /// ... 80 | /// if !childCoordinators.child(matching: SettingsCoordinator.self) { 81 | /// ... 82 | /// } 83 | /// ... 84 | public extension Dictionary where Value == Coordinating { 85 | 86 | /// Access the first child matching a specific type. 87 | /// - Parameter type: the type of the child 88 | /// - Returns: the first child coordinator or nil 89 | func child(matching type: T.Type) -> T? { 90 | values.first { $0 is T } as? T 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Coordinator/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // Radiant Tap Essentials 4 | // 5 | // Copyright © 2016 Radiant Tap 6 | // MIT License · http://choosealicense.com/licenses/mit/ 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | /// Simple closure which allows you to wrap any coordinatingResponder method and 13 | /// add it into a `queuedMessages` array on the Coordinator. 14 | /// 15 | /// You need to do this in case method needs a dependency that may not be available 16 | /// at that particular moment. So save it until dependencies are updated. 17 | public typealias CoordinatingQueuedMessage = () -> Void 18 | 19 | 20 | /** 21 | Coordinators are a design pattern that encourages decoupling view controllers 22 | in such a way that they know as little as possible about how they are presented. 23 | View Controllers should never directly push/pop or present other VCs. 24 | They should not be aware of their existence. 25 | 26 | **That is Coordinator's job.** 27 | 28 | Coordinators can be “nested” such that child coordinators encapsulate different flows 29 | and prevent any one of them from becoming too large. 30 | 31 | Each coordinator has an identifier to simplify logging and debugging. 32 | Identifier is also used as key for the `childCoordinators` dictionary. 33 | 34 | You should never use this class directly (although you can). 35 | Make a proper subclass and add specific behavior for the given particular usage. 36 | 37 | Note: Don't overthink this. Idea is to have fairly small number of coordinators in the app. 38 | If you embed controllers into other VC (thus using them as simple UI components), 39 | then keep that flow inside the given container controller. 40 | Expose to Coordinator only those behaviors that cause push/pop/present to bubble up. 41 | */ 42 | 43 | 44 | /// Main Coordinator instance, where T is UIViewController or any of its subclasses. 45 | @MainActor 46 | open class Coordinator: UIResponder, Coordinating { 47 | public let rootViewController: T 48 | 49 | 50 | /// You need to supply UIViewController (or any of its subclasses) that will be loaded as root of the UI hierarchy. 51 | /// Usually one of container controllers (UINavigationController, UITabBarController etc). 52 | /// 53 | /// - parameter rootViewController: UIViewController at the top of the hierarchy. 54 | /// - returns: Coordinator instance, fully prepared but started yet. 55 | /// 56 | /// Note: if you override this init, you must call `super`. 57 | public init(rootViewController: T?) { 58 | guard let rvc = rootViewController else { 59 | preconditionFailure("Must supply UIViewController (or any of its subclasses) or override this init and instantiate VC in there.") 60 | } 61 | self.rootViewController = rvc 62 | super.init() 63 | } 64 | 65 | 66 | open lazy var identifier: String = { 67 | return String(describing: type(of: self)) 68 | }() 69 | 70 | 71 | /// Next coordinatingResponder for any Coordinator instance is its parent Coordinator. 72 | open override var coordinatingResponder: UIResponder? { 73 | return parent as? UIResponder 74 | } 75 | 76 | 77 | 78 | 79 | // MARK:- Lifecycle 80 | 81 | private(set) public var isStarted: Bool = false 82 | 83 | /// Tells the coordinator to create/display its initial view controller and take over the user flow. 84 | /// Use this method to configure your `rootViewController` (if it isn't already). 85 | /// 86 | /// Some examples: 87 | /// * instantiate and assign `viewControllers` for UINavigationController or UITabBarController 88 | /// * assign itself (Coordinator) as delegate for the shown UIViewController(s) 89 | /// * setup closure entry/exit points 90 | /// etc. 91 | /// 92 | /// - Parameter completion: An optional `Callback` executed at the end. 93 | /// 94 | /// Note: if you override this method, you must call `super` and pass the `completion` closure. 95 | open func start() { 96 | rootViewController.parentCoordinator = self 97 | isStarted = true 98 | } 99 | 100 | /// Tells the coordinator that it is done and that it should 101 | /// clear out its backyard. 102 | /// 103 | /// Possible stuff to do here: dismiss presented controller or pop back pushed ones. 104 | /// 105 | /// - Parameter completion: Closure to execute at the end. 106 | /// 107 | /// Note: if you override this method, you must call `super` and pass the `completion` closure. 108 | open func stop() { 109 | rootViewController.parentCoordinator = nil 110 | } 111 | 112 | /// By default, calls `stopChild` on the given Coordinator, passing in the `completion` block. 113 | /// 114 | /// (See also comments for this method in the Coordinating protocol) 115 | /// 116 | /// Note: if you override this method, you should call `super` and pass the `completion` closure. 117 | open func coordinatorDidFinish(_ coordinator: Coordinating) { 118 | stopChild(coordinator: coordinator) 119 | } 120 | 121 | /// Coordinator can be in memory, but it‘s not currently displaying anything. 122 | /// For example, parentCoordinator started some other Coordinator which then took over root VC to display its VCs, 123 | /// but did not stop this one. 124 | /// 125 | /// Parent Coordinator can then re-activate this one, in which case it should take-over the 126 | /// the ownership of the root VC. 127 | /// 128 | /// Note: if you override this method, you should call `super` 129 | /// 130 | /// By default, it sets itself as `parentCoordinator` for its `rootViewController`. 131 | open func activate() { 132 | rootViewController.parentCoordinator = self 133 | } 134 | 135 | /// This should activate relevant Coordinator + remove any shown UIVCs from other Coordinators. 136 | /// 137 | /// By default, it just calls `activate()` 138 | open func takeover() { 139 | activate() 140 | } 141 | 142 | 143 | 144 | // MARK:- Containment 145 | 146 | open weak var parent: Coordinating? 147 | 148 | /// A dictionary of child Coordinators, where key is Coordinator's identifier property. 149 | /// The only way to add/remove something is through `startChild` / `stopChild` methods. 150 | private(set) public var childCoordinators: [String: Coordinating] = [:] 151 | 152 | /** 153 | Adds new child coordinator and starts it. 154 | 155 | - Parameter coordinator: The coordinator implementation to start. 156 | - Parameter completion: An optional `Callback` passed to the coordinator's `start()` method. 157 | */ 158 | public func startChild(coordinator: Coordinating) { 159 | childCoordinators[coordinator.identifier] = coordinator 160 | coordinator.parent = self 161 | coordinator.start() 162 | } 163 | 164 | 165 | /** 166 | Stops the given child coordinator and removes it from the `childCoordinators` array 167 | 168 | - Parameter coordinator: The coordinator implementation to stop. 169 | - Parameter completion: An optional `Callback` passed to the coordinator's `stop()` method. 170 | */ 171 | public func stopChild(coordinator: Coordinating) { 172 | coordinator.parent = nil 173 | self.childCoordinators.removeValue(forKey: coordinator.identifier) 174 | coordinator.stop() 175 | } 176 | 177 | 178 | // MARK:- Queuing coordinatingResponder methods 179 | 180 | /// Temporary keeper for methods requiring dependency which is not available yet. 181 | private(set) public var queuedMessages: [CoordinatingQueuedMessage] = [] 182 | 183 | /// Simply add the message wrapped in the closure. Mind the capture list for `self` and other objects. 184 | public func enqueueMessage(_ message: @escaping CoordinatingQueuedMessage ) { 185 | queuedMessages.append( message ) 186 | } 187 | 188 | /// Call this each time your Coordinator's dependencies are updated. 189 | /// It will go through all the queued closures and try to execute them again. 190 | public func processQueuedMessages() { 191 | // make a local copy 192 | let arr = queuedMessages 193 | // clean up the queue, in case it's re-populated while this pass is ongoing 194 | queuedMessages.removeAll() 195 | // execute each message 196 | arr.forEach { $0() } 197 | } 198 | } 199 | 200 | -------------------------------------------------------------------------------- /Sources/Coordinator/NavigationCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationCoordinator.swift 3 | // Radiant Tap Essentials 4 | // 5 | // Copyright © 2017 Radiant Tap 6 | // MIT License · http://choosealicense.com/licenses/mit/ 7 | // 8 | 9 | import UIKit 10 | 11 | @MainActor 12 | open class NavigationCoordinator: Coordinator, UINavigationControllerDelegate { 13 | // References to actual UIViewControllers managed by this Coordinator instance. 14 | open var viewControllers: [UIViewController] = [] 15 | 16 | /// This method is implemented to detect when customer "pop" back using UINC's backButtonItem. 17 | /// (Need to detect that in order to remove popped VC from Coordinator's `viewControllers` array.) 18 | /// 19 | /// It is strongly advised to *not* override this method, but it's allowed to do so in case you really need to. 20 | /// What you likely want to override is `handlePopBack(to:)` method. 21 | open func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { 22 | // By the moment this method is called, UINC's viewControllers is already updated. 23 | // If it was push, `viewControllers` will contain this shown `viewController`. 24 | // For pop, VC is already removed. 25 | 26 | guard let transitionCoordinator = navigationController.transitionCoordinator else { 27 | // TransitionCoordinator is not present, most likely because `popViewController(animated: false)` is called. 28 | // 29 | // In the Coordinator-based app, this should *NOT* be done, ever. 30 | return 31 | } 32 | 33 | // If transitionCoordinator is present, it's an animated push or pop. 34 | // Check if FROM ViewController is still present in NC's viewControllers list; 35 | // if it is, it means that this is push and we don't care about this. 36 | guard 37 | let fromViewController = transitionCoordinator.viewController(forKey: .from), 38 | !navigationController.viewControllers.contains(fromViewController) 39 | else { 40 | return 41 | } 42 | 43 | self.didPopTransition(to: viewController) 44 | } 45 | 46 | /// If you subclass NavigationCoordinator, then override this method if you need to 47 | /// do something special when customer taps the UIKit's backButton in the navigationBar. 48 | /// 49 | /// By default, this does nothing. 50 | open func handlePopBack(to vc: UIViewController?) { 51 | } 52 | 53 | 54 | // MARK:- Presenting 55 | 56 | public func present(_ vc: UIViewController, animated: Bool = true, completion: (() -> Void)? = nil) { 57 | vc.parentCoordinator = self 58 | rootViewController.present(vc, animated: animated, completion: completion) 59 | } 60 | 61 | public func dismiss(animated: Bool = true, completion: (() -> Void)? = nil) { 62 | rootViewController.dismiss(animated: animated, completion: completion) 63 | } 64 | 65 | 66 | // MARK:- Navigating 67 | 68 | /// Main method to push supplied UIVC to the navigation stack. 69 | /// First it adds the `vc` to the Coordinator's `viewControllers` then calls `show(vc)` on the root. 70 | public func show(_ vc: UIViewController) { 71 | viewControllers.append(vc) 72 | rootViewController.show(vc, sender: self) 73 | } 74 | 75 | /// Clears entire navigation stack on both the Coordinator and UINavigationController by 76 | /// setting this `[vc]` on respective `viewControllers` property. 77 | public func root(_ vc: UIViewController) { 78 | viewControllers = [vc] 79 | rootViewController.viewControllers = [vc] 80 | } 81 | 82 | /// Replaces current top UIVC in the navigation stack (currently visible UIVC) in the root 83 | /// with the supplied `vc` instance. 84 | public func top(_ vc: UIViewController, animated: Bool = true) { 85 | if viewControllers.count == 0 { 86 | root(vc) 87 | return 88 | } 89 | viewControllers.removeLast() 90 | 91 | if animated { 92 | rootViewController.viewControllers.removeLast() 93 | show(vc) 94 | } else { 95 | viewControllers.append(vc) 96 | rootViewController.viewControllers = viewControllers 97 | } 98 | } 99 | 100 | /// Pops back to previous UIVC in the stack, inside this Coordinator. 101 | public func pop(animated: Bool = true) { 102 | // there must be at least two VCs in order for UINC.pop to succeed (you can't pop the last VC in the stack) 103 | if viewControllers.count < 2 { 104 | return 105 | } 106 | viewControllers = Array(viewControllers.dropLast()) 107 | 108 | rootViewController.popViewController(animated: animated) 109 | } 110 | 111 | /// Pops back to the given instance, removing one or more UIVCs from the navigation stack. 112 | public func pop(to vc: UIViewController, animated: Bool = true) { 113 | guard let index = viewControllers.firstIndex(of: vc) else { return } 114 | 115 | let lastPosition = viewControllers.count - 1 116 | if lastPosition - index <= 0 { 117 | return 118 | } 119 | 120 | viewControllers = Array(viewControllers.dropLast(lastPosition - index)) 121 | rootViewController.popToViewController(vc, animated: animated) 122 | } 123 | 124 | 125 | // MARK:- Coordinator lifecycle 126 | 127 | open override func start() { 128 | // assign itself as UINavigationControllerDelegate 129 | rootViewController.delegate = self 130 | // must call this 131 | super.start() 132 | } 133 | 134 | open override func stop() { 135 | // relinquish being delegate for UINC 136 | rootViewController.delegate = nil 137 | 138 | // remove all of its UIVCs from the root UINC 139 | for vc in viewControllers { 140 | guard let index = rootViewController.viewControllers.firstIndex(of: vc) else { continue } 141 | rootViewController.viewControllers.remove(at: index) 142 | } 143 | 144 | // clean up UIVC instances 145 | viewControllers.removeAll() 146 | 147 | // must call this 148 | super.stop() 149 | } 150 | 151 | override open func coordinatorDidFinish(_ coordinator: Coordinating) { 152 | // some child Coordinator reports that it's done 153 | // (pop-ed back from, most likely) 154 | 155 | super.coordinatorDidFinish(coordinator) 156 | 157 | // figure out which Coordinator should now take ownershop of root NC 158 | guard let topVC = self.rootViewController.topViewController else { 159 | return 160 | } 161 | // if it belongs to this Coordinator, then re-activate itself 162 | if self.viewControllers.contains(topVC) { 163 | self.activate() 164 | self.handlePopBack(to: topVC) 165 | return 166 | } 167 | 168 | // if not, go through other possible child Coordinators 169 | for (_, c) in self.childCoordinators { 170 | if 171 | let c = c as? NavigationCoordinator, 172 | c.viewControllers.contains(topVC) 173 | { 174 | c.activate() 175 | c.handlePopBack(to: topVC) 176 | return 177 | } 178 | } 179 | 180 | // if nothing found, then this Coordinator is also done, along with its child 181 | self.parent?.coordinatorDidFinish(self) 182 | } 183 | 184 | open override func activate() { 185 | // take back ownership over root (UINavigationController) 186 | super.activate() 187 | 188 | // assign itself again as `UINavigationControllerDelegate` 189 | rootViewController.delegate = self 190 | } 191 | 192 | /// Activates existing Coordinator instance by assigning itself as UINCDelegate for the rootViewController. (read: calls `activate()`) 193 | /// Then installs its `viewControllers` on (root) UINavigationController, effectivelly clearing out its previous stack. 194 | /// 195 | /// Call this when you want to entirely replace one Coordinator instance with another Coordinator. 196 | open override func takeover() { 197 | activate() 198 | 199 | // re-assign own content View Controllers, clearing out whatever is there 200 | rootViewController.viewControllers = viewControllers 201 | } 202 | } 203 | 204 | private extension NavigationCoordinator { 205 | func didPopTransition(to viewController: UIViewController) { 206 | // Check: is there any controller left shown in this Coordinator? 207 | if viewControllers.count == 0 { 208 | // there isn't thus inform the parent Coordinator that this child Coordinator is done. 209 | parent?.coordinatorDidFinish(self) 210 | return 211 | } 212 | 213 | // If VC, which was just shown, is the last in this Coordinator's stack, 214 | // then nothing to do, because the other VC (which was pop-ed) was not in this Coordinator's domain. 215 | // | If this actually happens, it likely points to a mistake somewhere else. 216 | // | (It means we had some `show(vc)` happen that _did not_ update this Coordinator's viewControllers, 217 | // | nor it switched to some other Coordinator which should have become UINC.delegate) 218 | if viewController === viewControllers.last { 219 | return 220 | } 221 | 222 | // Check: is VC present in Coordinator's viewControllers sequence? 223 | // | Note: using firstIndex(of:) and not .last nicely handles if you programatically pop more than one UIVC. 224 | guard let index = viewControllers.firstIndex(of: viewController) else { 225 | // it's not, it means UINC moved to some other Coordinator domain and thus bail out from here 226 | parent?.coordinatorDidFinish(self) 227 | return 228 | } 229 | 230 | let lastIndex = viewControllers.count - 1 231 | if lastIndex <= index { 232 | return 233 | } 234 | viewControllers = Array(viewControllers.dropLast(lastIndex - index)) 235 | 236 | handlePopBack(to: viewController) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Sources/Coordinator/UIKit-CoordinatingExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKit-CoordinatingExtensions.swift 3 | // Radiant Tap Essentials 4 | // 5 | // Copyright © 2016 Radiant Tap 6 | // MIT License · http://choosealicense.com/licenses/mit/ 7 | // 8 | 9 | import UIKit 10 | 11 | // Inject parentCoordinator property into all UIViewControllers 12 | extension UIViewController { 13 | private class WeakCoordinatingTrampoline: NSObject { 14 | weak var coordinating: Coordinating? 15 | } 16 | 17 | @MainActor 18 | private struct AssociatedKeys { 19 | // per: https://github.com/atrick/swift-evolution/blob/diagnose-implicit-raw-bitwise/proposals/nnnn-implicit-raw-bitwise-conversion.md#workarounds-for-common-cases 20 | static var ParentCoordinator: Void? 21 | } 22 | 23 | public weak var parentCoordinator: Coordinating? { 24 | get { 25 | let trampoline = objc_getAssociatedObject(self, &AssociatedKeys.ParentCoordinator) as? WeakCoordinatingTrampoline 26 | return trampoline?.coordinating 27 | } 28 | set { 29 | let trampoline = WeakCoordinatingTrampoline() 30 | trampoline.coordinating = newValue 31 | objc_setAssociatedObject(self, &AssociatedKeys.ParentCoordinator, trampoline, .OBJC_ASSOCIATION_RETAIN) 32 | } 33 | } 34 | } 35 | 36 | 37 | 38 | 39 | /** 40 | Driving engine of the message passing through the app, with no need for Delegate pattern nor Singletons. 41 | 42 | It piggy-backs on the `UIResponder.next` in order to pass the message through UIView/UIVC hierarchy of any depth and complexity. 43 | However, it does not interfere with the regular `UIResponder` functionality. 44 | 45 | At the `UIViewController` level (see below), it‘s intercepted to switch up to the coordinator, if the UIVC has one. 46 | Once that happens, it stays in the `Coordinator` hierarchy, since coordinator can be nested only inside other coordinators. 47 | */ 48 | extension UIResponder { 49 | @objc open var coordinatingResponder: UIResponder? { 50 | return next 51 | } 52 | 53 | /* 54 | // sort-of implementation of the custom message/command to put into your Coordinable extension 55 | 56 | func messageTemplate(args: Whatever, sender: Any? = nil) { 57 | coordinatingResponder?.messageTemplate(args: args, sender: sender) 58 | } 59 | */ 60 | } 61 | 62 | extension UIResponder { 63 | /// Searches upwards the responder chain for the `Coordinator` that manages current `UIViewController` 64 | public var containingCoordinator: Coordinating? { 65 | if let vc = self as? UIViewController, let pc = vc.parentCoordinator { 66 | return pc 67 | } 68 | 69 | return coordinatingResponder?.containingCoordinator 70 | } 71 | } 72 | 73 | 74 | extension UIViewController { 75 | /** 76 | Returns `parentCoordinator` if this controller has one, 77 | or its parent `UIViewController` if it has one, 78 | or its view's `superview`. 79 | 80 | Copied from `UIResponder.next` documentation: 81 | 82 | - The `UIResponder` class does not store or set the next responder automatically, 83 | instead returning nil by default. 84 | 85 | - Subclasses must override this method to set the next responder. 86 | 87 | - UIViewController implements the method by returning its view’s superview; 88 | - UIWindow returns the application object, and UIApplication returns nil. 89 | */ 90 | override open var coordinatingResponder: UIResponder? { 91 | guard let parentCoordinator = self.parentCoordinator else { 92 | guard let parentController = self.parent else { 93 | guard let presentingController = self.presentingViewController else { 94 | return view.superview 95 | } 96 | return presentingController as UIResponder 97 | } 98 | return parentController as UIResponder 99 | } 100 | return parentCoordinator as? UIResponder 101 | } 102 | } 103 | 104 | -------------------------------------------------------------------------------- /documentation/Class.md: -------------------------------------------------------------------------------- 1 | [Coordinator](../README.md) : the [Pattern](Pattern.md) · the [Library](Library.md) · the **Class** · recommended [Implementation](Implement.md) 2 | 3 | ## Coordinator: the class 4 | 5 | `Coordinator` class is parameterized with UIViewController subclass it uses as root VC. 6 | (`Coordinating` protocol exists mainly to avoid issues with generic classes and collections.) 7 | 8 | ```swift 9 | open class Coordinator: UIResponder, Coordinating { 10 | public init(rootViewController: T?) { ... } 11 | 12 | open override var coordinatingResponder: UIResponder? { 13 | return parent as? UIResponder 14 | } 15 | } 16 | ``` 17 | 18 | Since apps can be fairly complex, each Coordinator can have an unlimited number of child Coordinators. Thus you can have `AccountCoordinator`, `PaymentCoordinator`, `CartCoordinator`, `CatalogCoordinator` and so on. There are no rules nor guidelines: group your related app screens under particular Coordinator umbrella as you see fit. 19 | 20 | When you instantiate a Coordinator instance, you call `start()` to use it and `stop()` when it’s no longer needed. 21 | 22 | In the Coordinator subclass, you’ll override the method you defined in the UIResponder extension and actually do something useful instead of just calling the same method on next `coordinatingResponder`. 23 | 24 | ### NavigationCoordinator 25 | 26 | This is the only concrete subclass this library offers and I encourage you to subclass it for your own needs. 27 | 28 | It uses `UINavigationController` as root VC and it keeps references to shown UIVCs in its own `viewControllers` property. This property shadows UINavigationController’s property of the same name but it is not cleared out until the NavigationCoordinator is stopped. This allows you to replace one NavigationCoordinator instance with another and saving and restoring their stack of UIVC instances in the process. 29 | 30 | If offers all the methods you may need when working with navigation pattern: 31 | 32 | · `root(_ vc: UIViewController)‌` — replaces entire navigation stack with just this one given UIVC. Perfect when switching from one multi-screen user flow (like say account creation process) to single view (say account confirmation code screen) 33 | 34 | · `show(_ vc: UIViewController)` — same as navigationController.show(). 35 | 36 | · `top(_ vc: UIViewController)‌` — replace the visible UIVC with the given one. Does not replace entire navigation stack, just the last UIVC (which is currently visible) 37 | 38 | · `‌pop(to vc: UIViewController, animated: Bool = true)` — programmatically pop the stack to the given UIVC instance, which should exist in the navigation stack. 39 | 40 | · `‌present(_ vc: UIViewController, animated: Bool = true, completion: (() -> Void)? = nil)` — `NavigationCoordinator` will setup itself as `parentCoordinator` for the given `vc` and then its root UINavigationController will present that `vc` 41 | 42 | · `‌dismiss(animated: Bool = true, completion: (() -> Void)? = nil)` — dismiss the currently presented UIVC. 43 | 44 | NavigationController gives you a chance to react to the customer tap on the Back button. Simply override this method and update your internal state: 45 | 46 | · `‌handlePopBack(to vc: UIViewController?)` -------------------------------------------------------------------------------- /documentation/Implement.md: -------------------------------------------------------------------------------- 1 | [Coordinator](../README.md) : the [Pattern](Pattern.md) · the [Library](Library.md) · the [Class](Class.md) · recommended **Implementation** 2 | 3 | ## Recommended implementation 4 | 5 | Since iOS 13, Apple strongly encourages iOS apps using scene-based structure and behavior. Any scene (window) can be created/terminated at any moment. Thus the only object in the app that will live as long as the app is alive is `AppDelegate` which will spawn a scene, when needed. Each scene should have at least one Coordinator instance, let’s call it `SceneCoordinator`. 6 | 7 | Thus the only place to keep app-wide dependencies is `AppDelegate`. 8 | There can be way too many of these dependencies to shuffle around thus it’s good idea to create a singular container for all of them: 9 | 10 | ### AppDependency 11 | 12 | A very simple conduit to keep all your non-UI dependencies in one struct, automatically accessible to any Coordinator, at any level. 13 | 14 | ```swift 15 | struct AppDependency { 16 | var webService: WebService? 17 | var dataManager: DataManager? 18 | var accountManager: AccountManager? 19 | var contentManager: ContentManager? 20 | 21 | // Init 22 | 23 | init( 24 | webService: WebService? = nil, 25 | dataManager: DataManager? = nil, 26 | accountManager: AccountManager? = nil, 27 | contentManager: ContentManager? = nil) 28 | { 29 | self.webService = webService 30 | self.dataManager = dataManager 31 | self.accountManager = accountManager 32 | self.contentManager = contentManager 33 | } 34 | } 35 | ``` 36 | 37 | Every time `appDependency` is updated on any Coordinator, it must pass the new value to all its child Coordinators. 38 | 39 | ```swift 40 | final class ContentCoordinator: NavigationCoordinator { 41 | var appDependency: AppDependency? { 42 | didSet { 43 | updateChildCoordinatorDependencies() 44 | } 45 | } 46 | 47 | ``` 48 | 49 | This way, Coordinators can fulfill their promise to be *routing mechanism* between any UIVC and any back-end object. 50 | 51 | ### AppDelegate 52 | 53 | As said before, instances of app-wide dependencies should then be kept here. 54 | 55 | ``` 56 | final class AppDelegate: UIResponder, UIApplicationDelegate { 57 | var webService: WebService(...) 58 | var dataManager: DataManager(...) 59 | ... 60 | 61 | 62 | var appDependency: AppDependency? { 63 | didSet { 64 | updateSceneDependencies() 65 | } 66 | } 67 | 68 | func updateSceneDependencies() {...} 69 | 70 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 71 | 72 | // instantiate all services and middleware 73 | // build and set AppDependency 74 | rebuildDependencies() 75 | 76 | return true 77 | } 78 | ``` 79 | 80 | Method `rebuildDependencies()` simply (re)creates AppDependency struct with whatever is currently available and assigns it to the said property. 81 | 82 | ### Pass dependencies from AppDelegate to scenes 83 | 84 | The following method is the only place where we can pass these dependencies down to `UISceneSession` (and thus to `UIScene`): 85 | 86 | ```swift 87 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 88 | 89 | connectingSceneSession.appDependency = appDependency 90 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 91 | } 92 | ``` 93 | 94 | For this to work, we need inject appDependency property into UISceneSession: 95 | 96 | ```swift 97 | extension UISceneSession { 98 | @MainActor 99 | private struct AssociatedKeys { 100 | static var appDependency: Void? 101 | } 102 | 103 | var appDependency: AppDependency? { 104 | get { 105 | return objc_getAssociatedObject(self, &AssociatedKeys.appDependency) as? AppDependency 106 | } 107 | set { 108 | objc_setAssociatedObject(self, &AssociatedKeys.appDependency, newValue, .OBJC_ASSOCIATION_COPY) 109 | sceneCoordinator?.appDependency = newValue 110 | } 111 | } 112 | } 113 | ``` 114 | 115 | And now this method in `AppDelegate` would work as well. 116 | 117 | ```swift 118 | func updateSceneDependencies() { 119 | let application = UIApplication.shared 120 | 121 | application.openSessions.forEach { 122 | $0.appDependency = appDependency 123 | } 124 | } 125 | 126 | ``` 127 | 128 | ### SceneCoordinator 129 | 130 | Here’s a typical Coordinator that uses `UINavigationController` as its root VC: 131 | 132 | ```swift 133 | @MainActor 134 | final class SceneCoordinator: NavigationCoordinator, NeedsDependency { 135 | private weak var scene: UIScene! 136 | private weak var sceneDelegate: SceneDelegate! 137 | 138 | init(scene: UIScene, 139 | sceneDelegate: SceneDelegate, 140 | appBundleIdentifier: String? = nil) 141 | { 142 | self.scene = scene 143 | self.sceneDelegate = sceneDelegate 144 | 145 | let vc = NavigationController() 146 | super.init(rootViewController: vc) 147 | 148 | appDependency = scene.session.appDependency 149 | } 150 | 151 | override var coordinatingResponder: UIResponder? { 152 | return sceneDelegate 153 | } 154 | ``` 155 | 156 | `SceneCoordinator` takes over management of `window`’s `rootViewController` continues the `coordinatingResponder` chain from here to `SceneDelegate`. 157 | 158 | ### SceneDelegate 159 | 160 | Now we can instantiate our `SceneCoordinator` and pass-on the supplied `AppDependency` from the scene’s session. 161 | 162 | ``` 163 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate { 164 | 165 | var window: UIWindow? 166 | private(set) var coordinator: SceneCoordinator? 167 | 168 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 169 | guard let windowScene = (scene as? UIWindowScene) else { return } 170 | 171 | let window = UIWindow(windowScene: windowScene) 172 | self.window = window 173 | 174 | let sceneCoordinator = SceneCoordinator(scene: scene, sceneDelegate: self) 175 | self.coordinator = sceneCoordinator 176 | 177 | window.rootViewController = sceneCoordinator.rootViewController 178 | 179 | window.makeKeyAndVisible() 180 | sceneCoordinator.start() 181 | } 182 | 183 | override var coordinatingResponder: UIResponder? { 184 | window 185 | } 186 | } 187 | ``` 188 | 189 | `SceneDelegate` also concludes the coordinatingResponder’s upward chain. 190 | 191 | ### Declarative routing 192 | 193 | Use an enum called `Page` inside `ApplicationCoordinator` to declare natural full UI screens of the app. 194 | 195 | Each specific screen will be UIVC instance. Each case can have zero or more associated values, which are public arguments / parameters for the screen. 196 | 197 | ```swift 198 | enum Page { 199 | case login 200 | case createAccount 201 | case confirmAccount(code: String?) 202 | case profile(user: User) 203 | ... 204 | } 205 | ``` 206 | 207 | Now you simply need one global method to switch to any screen in the app: 208 | 209 | ```swift 210 | extension UIResponder { 211 | @objc func globalDisplay(page: PageBox, sender: Any? = nil) { 212 | coordinatingResponder?.globalDisplay(page: page, sender: sender) 213 | } 214 | } 215 | ``` 216 | 217 | Override this method anywhere in the coordinatingResponder chain to do what is needed. In this case, usually at any Coordinator to push related UIVC instance. 218 | 219 | We need `PageBox` wrapper since the method arguments must be ObjC-friendly. This is super easy to do, giving us wrap and unwrap: 220 | 221 | ```swift 222 | final class PageBox: NSObject { 223 | let unbox: Page 224 | init(_ value: Page) { 225 | self.unbox = value 226 | } 227 | } 228 | extension Page { 229 | var boxed: PageBox { return PageBox(self) } 230 | } 231 | ``` 232 | 233 | This is also easy to script with tools like [Sourcery](https://github.com/krzysztofzablocki/Sourcery/). 234 | 235 | ### Naming your coordinatingResponder methods 236 | 237 | I use consistent naming scheme to group my coordinatingResponder methods. Anything that affects entire app is prefixed with `global` like `globalDisplay(page:sender:)` method above. 238 | 239 | All stuff dealing with shopping cart can use `cart` prefix. Same with account, catalog etc. Xcode’s autocomplete then helps to filter possible options when coding. 240 | 241 | (Sometimes a little consistency and common sense is enough.) 242 | 243 | -------------------------------------------------------------------------------- /documentation/Library.md: -------------------------------------------------------------------------------- 1 | [Coordinator](../README.md) : the [Pattern](Pattern.md) · the **Library** · the [Class](Class.md) · recommended [Implementation](Implement.md) 2 | 3 | ## Coordinator: the library 4 | 5 | Per this library, Coordinator instance is essentially defined by these two points: 6 | 7 | · **1** · It has one instance of `UIViewController` which is its *root ViewController*. This is usually some container controller like `UINavigationController` but it can be any subclass. 8 | 9 | This way, it can internally create instances of UIVC, populate their input with data it needs and then just _show_ or _present_ them as needed. By reusing these essential UIKit mechanisms it minimally interferes with how iOS already works. 10 | 11 | Coordinator takes care of navigation and routing while View Controller takes care of UI controls, touches and their corresponding events. 12 | 13 | · **2** · It subclasses `UIResponder`, same as `UIView` and `UIViewController` do. 14 | 15 | This is crucial. Library [extends UIResponder](https://github.com/radianttap/Coordinator/blob/master/Sources/Coordinator/UIKit-CoordinatingExtensions.swift) by giving it a new property called `coordinatingResponder`. This means that if you define a method like this: 16 | 17 | ```swift 18 | extension UIResponder { 19 | 20 | @MainActor 21 | @objc func accountLogin(username: String, 22 | password: String, 23 | sender: Any?) async throws 24 | { 25 | try await coordinatingResponder?.accountLogin( 26 | username: username, 27 | password: password, 28 | sender: sender 29 | ) 30 | } 31 | 32 | } 33 | ``` 34 | 35 | you can 36 | 37 | * Call `accountLogin()` from *anywhere*: view controller, view, button's event handler, table/collection view cell, UIAlertAction etc. 38 | * That call will be passed *up* the responder chain until it reaches some Coordinator instance which overrides that method. It none does, it gets to `UISceneDelegate` / `UIApplicationDelegate` (which is the top UI point your app is given by iOS runtime) and nothing happens. 39 | * At any point in this chain you can override this method, do whatever you want and continue the chain (or not, as you need) 40 | 41 | There is no need for Delegate pattern (although nothing stops you from using one). No other pattern is required, ever. 42 | 43 | By reusing the essential component of UIKit design — the responder chain — UIViewController's output can travel through the… 44 | 45 | ### CoordinatingResponder chain 46 | 47 | This is all that’s required for the chain to work: 48 | 49 | ```swift 50 | public extension UIResponder { 51 | @objc public var coordinatingResponder: UIResponder? { 52 | return next 53 | } 54 | } 55 | ``` 56 | 57 | That bit covers all the `UIView` subclasses: all the cells, buttons and other controls. 58 | 59 | Then on `UIViewController` level, this is specialized further: 60 | 61 | ```swift 62 | extension UIViewController { 63 | override open var coordinatingResponder: UIResponder? { 64 | guard let parentCoordinator = self.parentCoordinator else { 65 | guard let parentController = self.parent else { 66 | guard let presentingController = self.presentingViewController else { 67 | return view.superview 68 | } 69 | return presentingController as UIResponder 70 | } 71 | return parentController as UIResponder 72 | } 73 | return parentCoordinator as? UIResponder 74 | } 75 | } 76 | 77 | ``` 78 | 79 | Once responder chain moves into UIViewController instances, it stays there regardless of how the UIVC was displayed on screen: pushed or presented or embedded, it does not matter. 80 | 81 | Once it reaches the Coordinator’s `rootViewController` then the method call is passed to the `parentCoordinator` of that root VC. 82 | 83 | ```swift 84 | extension UIViewController { 85 | public weak var parentCoordinator: Coordinating? { 86 | get { ... } 87 | set { ... } 88 | } 89 | } 90 | ``` 91 | 92 | So this is how the chain is closed up. Which brings us to the `Coordinator` class. 93 | -------------------------------------------------------------------------------- /documentation/Pattern.md: -------------------------------------------------------------------------------- 1 | [Coordinator](../README.md) : the **Pattern** · the [Library](Library.md) · the [Class](Class.md) · recommended [Implementation](Implement.md) 2 | 3 | ## Coordinator: the pattern 4 | (and why you need to use it) 5 | 6 | [`UIViewController`](https://developer.apple.com/documentation/uikit/uiviewcontroller), in essence, is very straight-forward implementation of MVC pattern: it is mediator between data model / data source of any kind and one `UIView`. It has two roles: 7 | 8 | - receives the _data_ and configure / deliver it to the `view` 9 | - respond to actions and events that occurred in the view 10 | - route that response back into data storage or into some other UIViewController instance 11 | 12 | No, I did not make off-by-one error. The 3rd item should not be there but it is in the form of [show](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621377-show), [showDetailViewController](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621432-showdetailviewcontroller), [performSegue](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621413-performsegue), [present](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621380-present) & [dismiss](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621505-dismiss), [navigationController](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621860-navigationcontroller), [tabBarController](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621169-tabbarcontroller) etc. Those methods and properties should never be found inside your UIViewController code. 13 | 14 | > UIViewController instance should *not care* nor it should *know* about any other instance of UIVC or anything else. 15 | 16 | It should only care about its input (data) and output (events and actions). 17 | It does not care who/what sent that input. It does not care who/what handles its output. 18 | 19 | **Coordinator** is an object which 20 | 21 | * instantiates UIVCs 22 | * feeds the input into them 23 | * receives the output from them 24 | 25 | It order to do so, it also: 26 | 27 | * keeps references to any data sources in the app 28 | * implements data and UI _flows_ it is responsible for 29 | * manages UI screens related to those flows (one screen == one UIVC) 30 | 31 | #### Example 32 | 33 | Login flow that some `AccountCoordinator` may implement: 34 | 35 | 1. create an instance of `LoginViewController` and display it 36 | 2. receive username/password from `LoginViewController` 37 | 3. send them to `AccountManager` 38 | 4. If `AccountManager` returns an error, deliver that error back to `LoginViewController` 39 | 5. If `AccountManager` returns a `User` instance, replace `LoginViewController` with `UserProfileViewController` 40 | 41 | In this scenario, `LoginViewController` does not know that `AccountManager` exists nor it ever references it. It also does not know that `AccountCoordinator` nor `UserProfileViewController` exist. 42 | 43 | --------------------------------------------------------------------------------