diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample.xcodeproj/project.pbxproj b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample.xcodeproj/project.pbxproj index 168edd9..aeb290e 100644 --- a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample.xcodeproj/project.pbxproj +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample.xcodeproj/project.pbxproj @@ -14,6 +14,9 @@ 176F3CB529B8BF71009C4987 /* ShapesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176F3CB429B8BF71009C4987 /* ShapesView.swift */; }; 176F3CB829B8C05B009C4987 /* ShapesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176F3CB729B8C05B009C4987 /* ShapesCoordinator.swift */; }; 176F3CBB29B8C162009C4987 /* ShapesRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176F3CBA29B8C162009C4987 /* ShapesRoute.swift */; }; + 17AABAED2A6D5CF400AFE8A7 /* SimpleShapesAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AABAEC2A6D5CF400AFE8A7 /* SimpleShapesAction.swift */; }; + 17AABAEF2A6D5E1500AFE8A7 /* CustomShapesAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AABAEE2A6D5E1500AFE8A7 /* CustomShapesAction.swift */; }; + 17AABAF12A6D5F2100AFE8A7 /* ShapesAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AABAF02A6D5F2100AFE8A7 /* ShapesAction.swift */; }; 17F1183529CC63B1004755DB /* SimpleShapesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F1183429CC63B1004755DB /* SimpleShapesView.swift */; }; 17F1183729CC63C0004755DB /* CustomShapesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F1183629CC63C0004755DB /* CustomShapesView.swift */; }; 17F1183B29CC6678004755DB /* SimpleShapesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F1183A29CC6678004755DB /* SimpleShapesCoordinator.swift */; }; @@ -35,6 +38,9 @@ 176F3CB429B8BF71009C4987 /* ShapesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesView.swift; sourceTree = ""; }; 176F3CB729B8C05B009C4987 /* ShapesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesCoordinator.swift; sourceTree = ""; }; 176F3CBA29B8C162009C4987 /* ShapesRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesRoute.swift; sourceTree = ""; }; + 17AABAEC2A6D5CF400AFE8A7 /* SimpleShapesAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleShapesAction.swift; sourceTree = ""; }; + 17AABAEE2A6D5E1500AFE8A7 /* CustomShapesAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomShapesAction.swift; sourceTree = ""; }; + 17AABAF02A6D5F2100AFE8A7 /* ShapesAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesAction.swift; sourceTree = ""; }; 17E1C1FF2A1BD86D00542AB9 /* SwiftUICoordinator.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SwiftUICoordinator.xctestplan; sourceTree = ""; }; 17F1183429CC63B1004755DB /* SimpleShapesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleShapesView.swift; sourceTree = ""; }; 17F1183629CC63C0004755DB /* CustomShapesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomShapesView.swift; sourceTree = ""; }; @@ -136,6 +142,7 @@ children = ( 176F3CB729B8C05B009C4987 /* ShapesCoordinator.swift */, 176F3CBA29B8C162009C4987 /* ShapesRoute.swift */, + 17AABAF02A6D5F2100AFE8A7 /* ShapesAction.swift */, ); path = Shapes; sourceTree = ""; @@ -152,6 +159,7 @@ children = ( 17F1183A29CC6678004755DB /* SimpleShapesCoordinator.swift */, 17F1183C29CC668F004755DB /* SimpleShapesRoute.swift */, + 17AABAEC2A6D5CF400AFE8A7 /* SimpleShapesAction.swift */, ); path = SimpleShapes; sourceTree = ""; @@ -161,6 +169,7 @@ children = ( 17F1183E29CC8A57004755DB /* CustomShapesCoordinator.swift */, 17F1184029CC8A84004755DB /* CustomShapesRoute.swift */, + 17AABAEE2A6D5E1500AFE8A7 /* CustomShapesAction.swift */, ); path = CustomShapes; sourceTree = ""; @@ -246,18 +255,21 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 17AABAEF2A6D5E1500AFE8A7 /* CustomShapesAction.swift in Sources */, 17F1183729CC63C0004755DB /* CustomShapesView.swift in Sources */, 17F1183B29CC6678004755DB /* SimpleShapesCoordinator.swift in Sources */, 17F1184729CC8F1A004755DB /* Tower.swift in Sources */, 17F1184329CC8BA6004755DB /* Triangle.swift in Sources */, 1732C9FC29A6607500C2BC1F /* SwiftUICoordinatorExampleApp.swift in Sources */, 17F1184529CC8CF6004755DB /* Star.swift in Sources */, + 17AABAF12A6D5F2100AFE8A7 /* ShapesAction.swift in Sources */, 176F3CB829B8C05B009C4987 /* ShapesCoordinator.swift in Sources */, 176F3CB529B8BF71009C4987 /* ShapesView.swift in Sources */, 17360A412A1275D600DB2296 /* FadeTransition.swift in Sources */, 17F1183D29CC668F004755DB /* SimpleShapesRoute.swift in Sources */, 17F1183529CC63B1004755DB /* SimpleShapesView.swift in Sources */, 176F3CBB29B8C162009C4987 /* ShapesRoute.swift in Sources */, + 17AABAED2A6D5CF400AFE8A7 /* SimpleShapesAction.swift in Sources */, 17F1183F29CC8A57004755DB /* CustomShapesCoordinator.swift in Sources */, 17F1184129CC8A84004755DB /* CustomShapesRoute.swift in Sources */, ); diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/CustomShapes/CustomShapesAction.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/CustomShapes/CustomShapesAction.swift new file mode 100644 index 0000000..d535626 --- /dev/null +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/CustomShapes/CustomShapesAction.swift @@ -0,0 +1,15 @@ +// +// CustomShapesAction.swift +// SwiftUICoordinatorExample +// +// Created by Erik Drobne on 23/07/2023. +// + +import Foundation +import SwiftUICoordinator + +enum CustomShapesAction: CoordinatorAction { + case triangle + case star + case tower +} diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/CustomShapes/CustomShapesCoordinator.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/CustomShapes/CustomShapesCoordinator.swift index 60c224d..e9946fa 100644 --- a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/CustomShapes/CustomShapesCoordinator.swift +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/CustomShapes/CustomShapesCoordinator.swift @@ -10,8 +10,8 @@ import SwiftUI import SwiftUI import SwiftUICoordinator -class CustomShapesCoordinator: NSObject, Coordinator, Navigator { - +class CustomShapesCoordinator: Routing { + // MARK: - Internal properties weak var parent: Coordinator? = nil @@ -25,15 +25,19 @@ class CustomShapesCoordinator: NSObject, Coordinator, Navigator { self.parent = parent self.navigationController = navigationController self.startRoute = startRoute - super.init() } - func navigate(to route: NavigationRoute) { - guard let route = route as? CustomShapesRoute else { - return + func handle(_ action: CoordinatorAction) { + switch action { + case CustomShapesAction.triangle: + try? show(route: .triangle) + case CustomShapesAction.star: + try? show(route: .star) + case CustomShapesAction.tower: + try? show(route: .tower) + default: + parent?.handle(action) } - - try? show(route: route) } } diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesAction.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesAction.swift new file mode 100644 index 0000000..008ab85 --- /dev/null +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesAction.swift @@ -0,0 +1,15 @@ +// +// ShapesAction.swift +// SwiftUICoordinatorExample +// +// Created by Erik Drobne on 23/07/2023. +// + +import Foundation +import SwiftUICoordinator + +enum ShapesAction: CoordinatorAction { + case simpleShapes + case customShapes + case featuredShape(NavigationRoute) +} diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesCoordinator.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesCoordinator.swift index 4bdd72d..76de7f4 100644 --- a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesCoordinator.swift +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesCoordinator.swift @@ -8,7 +8,7 @@ import SwiftUI import SwiftUICoordinator -class ShapesCoordinator: NSObject, Coordinator, Navigator { +class ShapesCoordinator: Routing { // MARK: - Internal properties @@ -23,32 +23,31 @@ class ShapesCoordinator: NSObject, Coordinator, Navigator { init(startRoute: ShapesRoute) { self.navigationController = NavigationController() self.startRoute = startRoute - super.init() setup() } - func navigate(to route: NavigationRoute) { - switch route { - case ShapesRoute.simpleShapes: + func handle(_ action: CoordinatorAction) { + switch action { + case ShapesAction.simpleShapes: let coordinator = makeSimpleShapesCoordinator() try? coordinator.start() - case ShapesRoute.customShapes: + case ShapesAction.customShapes: let coordinator = makeCustomShapesCoordinator() try? coordinator.start() - case ShapesRoute.featuredShape(let route): + case let ShapesAction.featuredShape(route): switch route { - case let shapeRoute as SimpleShapesRoute: + case let shapeRoute as SimpleShapesRoute where shapeRoute != .simpleShapes: let coordinator = makeSimpleShapesCoordinator() coordinator.append(routes: [.simpleShapes, shapeRoute]) - case let shapeRoute as CustomShapesRoute: + case let shapeRoute as CustomShapesRoute where shapeRoute != .customShapes: let coordinator = makeCustomShapesCoordinator() coordinator.append(routes: [.customShapes, shapeRoute]) default: return } default: - return + break } } diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesRoute.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesRoute.swift index 714d7e6..c58fc7e 100644 --- a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesRoute.swift +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/Shapes/ShapesRoute.swift @@ -12,7 +12,7 @@ enum ShapesRoute: NavigationRoute { case shapes case simpleShapes case customShapes - case featuredShape(NavigationRoute) + case featuredShape var title: String? { switch self { diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesAction.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesAction.swift new file mode 100644 index 0000000..9e16a3a --- /dev/null +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesAction.swift @@ -0,0 +1,17 @@ +// +// SimpleShapesAction.swift +// SwiftUICoordinatorExample +// +// Created by Erik Drobne on 23/07/2023. +// + +import Foundation +import SwiftUICoordinator + +enum SimpleShapesAction: CoordinatorAction { + case rect + case roundedRect + case capsule + case ellipse + case circle +} diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesCoordinator.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesCoordinator.swift index c49d921..2ee9404 100644 --- a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesCoordinator.swift +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesCoordinator.swift @@ -8,7 +8,7 @@ import SwiftUI import SwiftUICoordinator -class SimpleShapesCoordinator: NSObject, Coordinator, Navigator { +class SimpleShapesCoordinator: Routing { // MARK: - Internal properties @@ -23,7 +23,6 @@ class SimpleShapesCoordinator: NSObject, Coordinator, Navigator { self.parent = parent self.navigationController = navigationController self.startRoute = startRoute - super.init() } func presentRoot() { @@ -35,12 +34,21 @@ class SimpleShapesCoordinator: NSObject, Coordinator, Navigator { routing.childCoordinators.removeAll() } - func navigate(to route: NavigationRoute) { - guard let route = route as? SimpleShapesRoute else { - return + func handle(_ action: CoordinatorAction) { + switch action { + case SimpleShapesAction.rect: + try? show(route: .rect) + case SimpleShapesAction.roundedRect: + try? show(route: .roundedRect) + case SimpleShapesAction.capsule: + try? show(route: .capsule) + case SimpleShapesAction.ellipse: + try? show(route: .ellipse) + case SimpleShapesAction.circle: + try? show(route: .circle) + default: + parent?.handle(action) } - - try? show(route: route) } } diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/CustomShapesView.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/CustomShapesView.swift index fc8d2f0..3c2b407 100644 --- a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/CustomShapesView.swift +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/CustomShapesView.swift @@ -42,15 +42,15 @@ extension CustomShapesView { var coordinator: Coordinator? func didTapTriangle() { - coordinator?.navigate(to: CustomShapesRoute.triangle) + coordinator?.handle(CustomShapesAction.triangle) } func didTapStar() { - coordinator?.navigate(to: CustomShapesRoute.star) + coordinator?.handle(CustomShapesAction.star) } func didTapTower() { - coordinator?.navigate(to: CustomShapesRoute.tower) + coordinator?.handle(CustomShapesAction.tower) } } } diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/ShapesView.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/ShapesView.swift index ec78059..37d55f2 100644 --- a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/ShapesView.swift +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/ShapesView.swift @@ -42,11 +42,11 @@ extension ShapesView { var coordinator: Coordinator? func didTapBuiltIn() { - coordinator?.navigate(to: ShapesRoute.simpleShapes) + coordinator?.handle(ShapesAction.simpleShapes) } func didTapCustom() { - coordinator?.navigate(to: ShapesRoute.customShapes) + coordinator?.handle(ShapesAction.customShapes) } func didTapFeatured() { @@ -60,7 +60,7 @@ extension ShapesView { return } - coordinator?.navigate(to: ShapesRoute.featuredShape(route)) + coordinator?.handle(ShapesAction.featuredShape(route)) } } } diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/SimpleShapesView.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/SimpleShapesView.swift index c16c29e..f49bbc9 100644 --- a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/SimpleShapesView.swift +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Views/SimpleShapesView.swift @@ -52,23 +52,23 @@ extension SimpleShapesView { var coordinator: Coordinator? func didTapRectangle() { - coordinator?.navigate(to: SimpleShapesRoute.rect) + coordinator?.handle(SimpleShapesAction.rect) } func didTapRoundedRectangle() { - coordinator?.navigate(to: SimpleShapesRoute.roundedRect) + coordinator?.handle(SimpleShapesAction.roundedRect) } func didTapCapsule() { - coordinator?.navigate(to: SimpleShapesRoute.capsule) + coordinator?.handle(SimpleShapesAction.capsule) } func didTapEllipse() { - coordinator?.navigate(to: SimpleShapesRoute.ellipse) + coordinator?.handle(SimpleShapesAction.ellipse) } func didTapCircle() { - coordinator?.navigate(to: SimpleShapesRoute.circle) + coordinator?.handle(SimpleShapesAction.circle) } } } diff --git a/README.md b/README.md index 1c7b0d2..c4e1b99 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The second challenge is related to popping to the root view. This can occur when ## 🏃 Implementation -coordinator-diagram +workflow ### Coordinator @@ -32,18 +32,34 @@ public protocol Coordinator: AnyObject { /// An array that stores references to any child coordinators. var childCoordinators: [Coordinator] { get set } - /// Adds a child coordinator to the coordinator's child coordinators list, if needed. + /// Takes action parameter and handles the `CoordinatorAction`. + func handle(_ action: CoordinatorAction) + /// Adds child coordinator to the list. func add(child: Coordinator) - /// Navigate to a specific route. - func navigate(to route: NavigationRoute) - /// Remove the coordinator from its parent's child coordinators list. - func finish() + /// Removes the coordinator from the list of children. + func remove(coordinator: Coordinator) +} +``` + +### CoordinatorAction + +This protocol defines the available actions for the coordinator. Views should only communicate with the coordinator using actions (unidirectional flow). + +**Protocol declaration** + +```Swift +public protocol CoordinatorAction {} + +/// Essential actions. +public enum Action: CoordinatorAction { + case done(Any) + case cancel(Any) } ``` ### NavigationRoute -This protocol defines the available routes for navigation within a coordinator flow, which should be implemented using enum types. +This protocol defines the available routes for navigation within a coordinator flow. **Protocol declaration** @@ -70,26 +86,24 @@ public typealias Routing = Coordinator & Navigator @MainActor public protocol Navigator: ObservableObject { associatedtype Route: NavigationRoute - + var navigationController: NavigationController { get } /// The starting route of the navigator. - var startRoute: Route? { get } + var startRoute: Route { get } - /// This method is called when the navigator should start navigating. + /// This method should be called to start the flow and to show the view for the `startRoute`. func start() throws - /// Navigate to a specific route. - /// It creates a view for the route and adds it to the navigation stack using the specified action (TransitionAction). + /// It creates a view for the route and adds it to the navigation stack. func show(route: Route) throws - /// Sets the navigation stack to a new array of routes. - /// It can be useful if you need to reset the entire navigation stack to a new set of views. + /// Creates views for routes, and replaces the navigation stack with the specified views. func set(routes: [Route], animated: Bool) - /// Append a new set of routes to the existing navigation stack. + /// Creates views for routes, and appends them on the navigation stack. func append(routes: [Route], animated: Bool) - /// Pop the current view from the navigation stack. + /// Pops the top view from the navigation stack. func pop(animated: Bool) - /// Pops all the views from the stack except the root view. + /// Pops all the views on the stack except the root view. func popToRoot(animated: Bool) - /// Dismiss the view that was presented modally. + /// Dismisses the view. func dismiss(animated: Bool) } ``` @@ -117,19 +131,14 @@ import SwiftUICoordinator ### Create Route First, create an enum with all the available routes for a particular flow. -In the following example, we have the `ShapesRoute` enum representing the main flows of our application. It offers routes to present shapes list, simple shapes list, custom shapes list and -a featured shape. - -We can also create a deep link by creating an enum case with the associated value of type `NavigationRoute` and handle -flow execution within the coordinator. +In the following example, we have the `ShapesRoute` enum representing the main flows of our application. It offers routes to present shapes list, simple shapes list, custom shapes list and a featured shape. ```Swift enum ShapesRoute: NavigationRoute { case shapes case simpleShapes case customShapes - /// Deep link - case featuredShape(NavigationRoute) + case featuredShape var title: String? { switch self { @@ -152,12 +161,27 @@ enum ShapesRoute: NavigationRoute { } ``` +### Create Action + +`ShapesAction` represents all the available actions that can be triggered by the view. + +```Swift +enum ShapesAction: CoordinatorAction { + case simpleShapes + case customShapes + /// Deep link + case featuredShape(NavigationRoute) +} +``` + +We can also make a deep link by adding an associated value of type `NavigationRoute` to the enum. + ### Create Coordinator -Our `ShapesCoordinator` has to conform to the `Navigator` protocol and implement the `navigate(to route: NavigationRoute)` to execute flow-specific logic on method execution. Root coordinator has to initialize `NavigationController`. +Our `ShapesCoordinator` has to conform to the `Routing` protocol and implement the `handle(_ action: CoordinatorAction)` method which executes flow-specific logic when the action is received. `ShapesCoordinator` is the root coordinator and therefore needs to initialize `NavigationController`. ```Swift -class ShapesCoordinator: NSObject, Coordinator, Navigator { +class ShapesCoordinator: Routing { // MARK: - Internal properties @@ -172,32 +196,31 @@ class ShapesCoordinator: NSObject, Coordinator, Navigator { init(startRoute: ShapesRoute) { self.navigationController = NavigationController() self.startRoute = startRoute - super.init() setup() } - func navigate(to route: NavigationRoute) { - switch route { - case ShapesRoute.simpleShapes: + func handle(_ action: CoordinatorAction) { + switch action { + case ShapesAction.simpleShapes: let coordinator = makeSimpleShapesCoordinator() try? coordinator.start() - case ShapesRoute.customShapes: + case ShapesAction.customShapes: let coordinator = makeCustomShapesCoordinator() try? coordinator.start() - case ShapesRoute.featuredShape(let route): + case let ShapesAction.featuredShape(route): switch route { - case let shapeRoute as SimpleShapesRoute: + case let shapeRoute as SimpleShapesRoute where shapeRoute != .simpleShapes: let coordinator = makeSimpleShapesCoordinator() coordinator.append(routes: [.simpleShapes, shapeRoute]) - case let shapeRoute as CustomShapesRoute: + case let shapeRoute as CustomShapesRoute where shapeRoute != .customShapes: let coordinator = makeCustomShapesCoordinator() coordinator.append(routes: [.customShapes, shapeRoute]) default: return } default: - return + break } } @@ -234,7 +257,7 @@ extension ShapesCoordinator: RouterViewFactory { ### Initialize ShapesCoordinator -We are going to create an instance of `ShapesCoordinator` (our root coordinator) and pass it's `UINavigationController` to the +We are going to create an instance of `ShapesCoordinator` (our root coordinator) and pass its `UINavigationController` to the `UIWindow`. Our start route for the coordinator is `ShapesRoute.shape`. ```Swift diff --git a/Sources/SwiftUICoordinator/Coordinator/Coordinator.swift b/Sources/SwiftUICoordinator/Coordinator/Coordinator.swift index bf12338..fc8ec10 100644 --- a/Sources/SwiftUICoordinator/Coordinator/Coordinator.swift +++ b/Sources/SwiftUICoordinator/Coordinator/Coordinator.swift @@ -5,16 +5,21 @@ // Created by Erik Drobne on 12/12/2022. // -import SwiftUI +import Foundation @MainActor public protocol Coordinator: AnyObject { + /// A property that stores a reference to the parent coordinator, if any. var parent: Coordinator? { get } + /// An array that stores references to any child coordinators. var childCoordinators: [Coordinator] { get set } + /// Takes action parameter and handles the `CoordinatorAction`. + func handle(_ action: CoordinatorAction) + /// Adds child coordinator to the list. func add(child: Coordinator) - func navigate(to route: NavigationRoute) - func finish() + /// Removes the coordinator from the list of children. + func remove(coordinator: Coordinator) } // MARK: - Extensions @@ -22,14 +27,14 @@ public protocol Coordinator: AnyObject { public extension Coordinator { // MARK: - Public methods - - func finish() { - parent?.childCoordinators.removeAll(where: { $0 === self }) - } func add(child: Coordinator) { if !childCoordinators.contains(where: { $0 === child }) { childCoordinators.append(child) } } + + func remove(coordinator: Coordinator) { + childCoordinators.removeAll(where: { $0 === coordinator }) + } } diff --git a/Sources/SwiftUICoordinator/Coordinator/CoordinatorAction.swift b/Sources/SwiftUICoordinator/Coordinator/CoordinatorAction.swift new file mode 100644 index 0000000..dba5c47 --- /dev/null +++ b/Sources/SwiftUICoordinator/Coordinator/CoordinatorAction.swift @@ -0,0 +1,16 @@ +// +// CoordinatorAction.swift +// +// +// Created by Erik Drobne on 23/07/2023. +// + +import Foundation + +public protocol CoordinatorAction {} + +/// Essential actions. +public enum Action: CoordinatorAction { + case done(Any) + case cancel(Any) +} diff --git a/Sources/SwiftUICoordinator/Navigator/Navigator.swift b/Sources/SwiftUICoordinator/Navigator/Navigator.swift index 3ebd45d..10fc358 100644 --- a/Sources/SwiftUICoordinator/Navigator/Navigator.swift +++ b/Sources/SwiftUICoordinator/Navigator/Navigator.swift @@ -14,14 +14,22 @@ public protocol Navigator: ObservableObject { associatedtype Route: NavigationRoute var navigationController: NavigationController { get } + /// The starting route of the navigator. var startRoute: Route { get } + /// This method should be called to start the flow and to show the view for the `startRoute`. func start() throws + /// It creates a view for the route and adds it to the navigation stack. func show(route: Route) throws + /// Creates views for routes, and replaces the navigation stack with the specified views. func set(routes: [Route], animated: Bool) + /// Creates views for routes, and appends them on the navigation stack. func append(routes: [Route], animated: Bool) + /// Pops the top view from the navigation stack. func pop(animated: Bool) + /// Pops all the views on the stack except the root view. func popToRoot(animated: Bool) + /// Dismisses the view. func dismiss(animated: Bool) } diff --git a/Tests/SwiftUICoordinatorTests/Mocks/MockCoordinator.swift b/Tests/SwiftUICoordinatorTests/Mocks/MockCoordinator.swift index 2be3d8a..8bb04ae 100644 --- a/Tests/SwiftUICoordinatorTests/Mocks/MockCoordinator.swift +++ b/Tests/SwiftUICoordinatorTests/Mocks/MockCoordinator.swift @@ -8,7 +8,8 @@ import SwiftUI @testable import SwiftUICoordinator -class MockCoordinator: NSObject, Coordinator, Navigator { +class MockCoordinator: Routing { + var parent: Coordinator? var childCoordinators = [Coordinator]() var navigationController: NavigationController @@ -18,10 +19,9 @@ class MockCoordinator: NSObject, Coordinator, Navigator { self.parent = parent self.navigationController = NavigationController() self.startRoute = startRoute - super.init() } - func navigate(to route: NavigationRoute) { + func handle(_ action: CoordinatorAction) { } } diff --git a/Tests/SwiftUICoordinatorTests/SwiftUICoordinatorTests.swift b/Tests/SwiftUICoordinatorTests/SwiftUICoordinatorTests.swift index 8f5458a..608c18c 100644 --- a/Tests/SwiftUICoordinatorTests/SwiftUICoordinatorTests.swift +++ b/Tests/SwiftUICoordinatorTests/SwiftUICoordinatorTests.swift @@ -14,12 +14,12 @@ import SwiftUI XCTAssertEqual(rootCoordinator.childCoordinators.count, 1) } - func testFinishChildCoordinator() { + func testRemoveChildCoordinator() { let rootCoordinator = MockCoordinator(parent: nil, startRoute: .circle) let childCoordinator = MockCoordinator(parent: rootCoordinator, startRoute: .rectangle) rootCoordinator.add(child: childCoordinator) - childCoordinator.finish() + rootCoordinator.remove(coordinator: childCoordinator) XCTAssertEqual(rootCoordinator.childCoordinators.count, 0) }