├── .codecov.yml ├── .github └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── .swiftlint.yml ├── CHANGELOG.md ├── Code ├── Errors │ └── RoutingError.swift ├── Extensions │ ├── DispatchQueue.swift │ ├── SwiftUIView+TypeErasure.swift │ ├── UIWindow+Animation.swift │ └── UIWindow+KeyWindow.swift ├── Helpers │ ├── PathMatcher.swift │ └── TransitionAnimation.swift ├── Models │ └── NavigationRoute.swift ├── Modules │ ├── RoutableModule.swift │ └── RoutableModulesFactory.swift ├── Protocols │ ├── MVVM │ │ └── Router+MVVM.swift │ ├── Routable.swift │ ├── Route.swift │ ├── Router.swift │ ├── RouterAuthenticationHandler.swift │ └── RouterErrorHandler.swift ├── Routing │ ├── Extensions │ │ ├── NavigationRouter+ErrorHandling.swift │ │ ├── NavigationRouter+NavigationHandling.swift │ │ ├── NavigationRouter+NavigationInterception.swift │ │ ├── NavigationRouter+PathMatcher.swift │ │ └── NavigationRouter+PrivateHelpers.swift │ └── NavigationRouter.swift └── Views │ └── RoutedLink.swift ├── DOCUMENTATION.md ├── LICENSE ├── NavigationRouter.podspec ├── NavigationRouter.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── NavigationRouter.xcscheme ├── NavigationRouter.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md ├── Supporting files └── NavigationRouter-Info.plist ├── TestApp ├── NavigationRouterTestApp.entitlements ├── Supporting files │ ├── TestApp-Info.plist │ ├── TestAppTests-Info.plist │ └── TestAppUITests-Info.plist ├── TestApp.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── NavigationRouterTestApp.xcscheme ├── TestApp │ ├── Delegates │ │ ├── AppDelegate.swift │ │ └── SceneDelegate.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Resources │ │ └── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ │ └── Contents.json │ └── Views │ │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ │ ├── View1A.swift │ │ ├── View2A.swift │ │ ├── ViewModel1A.swift │ │ └── ViewModel2A.swift ├── Tests │ └── NavigationRouterTests.swift └── UITests │ └── NavigationRouterUITests.swift ├── TestFeature1 ├── TestFeature1.xcodeproj │ └── project.pbxproj └── TestFeature1 │ ├── Info.plist │ ├── TestFeature1Module.swift │ ├── ViewModels │ ├── ViewModel1A.swift │ ├── ViewModel1B.swift │ └── ViewModel1C.swift │ └── Views │ ├── View1A.swift │ ├── View1B.swift │ └── View1C.swift ├── TestFeature2 ├── TestFeature2.xcodeproj │ └── project.pbxproj └── TestFeature2 │ ├── Info.plist │ ├── TestFeature2Module.swift │ ├── ViewModels │ ├── ViewModel2A.swift │ ├── ViewModel2B.swift │ ├── ViewModel2C.swift │ ├── ViewModel2D.swift │ └── ViewModel2E.swift │ └── Views │ ├── View2A.swift │ ├── View2B.swift │ ├── View2C.swift │ ├── View2D.swift │ └── View2E.swift └── TestFeature3 ├── TestFeature3.xcodeproj └── project.pbxproj └── TestFeature3 ├── Info.plist ├── TestFeature3Module.swift ├── ViewModels ├── ViewModel3A.swift ├── ViewModel3B.swift ├── ViewModel3C.swift ├── ViewModel3D.swift └── ViewModel3E.swift └── Views ├── View3A.swift ├── View3B.swift ├── View3C.swift ├── View3D.swift └── View3E.swift /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - Tests/ 3 | - TestApp/ 4 | - TestFeature1/ 5 | - TestFeature2/ 6 | - TestFeature3/ 7 | - Package.swift -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: build 6 | runs-on: macOS-latest 7 | steps: 8 | - name: Checkout project 9 | uses: actions/checkout@master 10 | - name: Resolve package dependencies 11 | run: xcodebuild -resolvePackageDependencies 12 | - name: Build project 13 | run: xcodebuild build -destination name="iPhone 11" -workspace "NavigationRouter.xcworkspace" -scheme "NavigationRouterTestApp" 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: test 6 | runs-on: macOS-latest 7 | steps: 8 | - name: Checkout project 9 | uses: actions/checkout@master 10 | - name: Resolve package dependencies 11 | run: xcodebuild -resolvePackageDependencies 12 | - name: Run tests 13 | run: xcodebuild test -destination name="iPhone 11" -workspace "NavigationRouter.xcworkspace" -scheme "NavigationRouterTestApp" 14 | - name: Upload coverage report 15 | run: bash <(curl -s https://codecov.io/bash) -X xcodellvm 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## Build generated 8 | .build/ 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | # Package.resolved 44 | .build/ 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | Pods/ 53 | Podfile.lock 54 | 55 | # Gems 56 | Gemfile.lock 57 | 58 | # Bluepill 59 | UIAutomationResults 60 | 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | Carthage/Checkouts 69 | Carthage/Build 70 | 71 | # Swift Package Manager 72 | Package.resolved 73 | 74 | # fastlane 75 | # 76 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 77 | # screenshots whenever they are needed. 78 | # For more information about the recommended setup visit: 79 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 80 | 81 | fastlane/report.xml 82 | fastlane/Preview.html 83 | fastlane/screenshots/**/*.png 84 | fastlane/coverage_output 85 | fastlane/test_output 86 | fastlane/README.md 87 | 88 | reports/ 89 | reports.swiftlint.txt 90 | 91 | .sonnarwork/ 92 | 93 | # Code Injection 94 | # 95 | # After new code Injection tools there's a generated folder /iOSInjectionProject 96 | # https://github.com/johnno1962/injectionforxcode 97 | 98 | iOSInjectionProject/ 99 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - closure_body_length 3 | - collection_alignment 4 | - conditional_returns_on_newline 5 | - duplicate_enum_cases 6 | - empty_string 7 | - fatal_error_message 8 | - literal_expression_end_indentation 9 | - missing_docs 10 | - modifier_order 11 | - multiline_literal_brackets 12 | - single_test_class 13 | - vertical_whitespace_between_cases 14 | disabled_rules: 15 | - trailing_whitespace 16 | - function_parameter_count 17 | excluded: 18 | - .build/ 19 | - Tests/ 20 | - TestApp/ 21 | - TestFeature1/ 22 | - TestFeature2/ 23 | - TestFeature3/ 24 | - Package.swift 25 | 26 | reporter: "xcode" 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.0.3](https://github.com/corteggo/NavigationRouter/releases/tag/1.0.3) 6 | 7 | Fixed a bug related to UITabBarController that was causing navigation not to work properly. 8 | 9 | 10 | ## [1.0.2](https://github.com/corteggo/NavigationRouter/releases/tag/1.0.2) 11 | 12 | Removed support for older versions than iOS 11.0 since Xcode 12 was complaining about SwiftUI in armv7. 13 | 14 | 15 | ## [1.0.1](https://github.com/corteggo/NavigationRouter/releases/tag/1.0.1) 16 | 17 | Removed unused Combine import that caused the project not to work with earlier versions than iOS 13.0. 18 | Removed support for iOS 8.0 since this was causing a warning. 19 | 20 | 21 | ## [1.0.0](https://github.com/corteggo/NavigationRouter/releases/tag/1.0.0) 22 | 23 | Initial version. 24 | -------------------------------------------------------------------------------- /Code/Errors/RoutingError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | /// Routing errors 24 | public enum RoutingError: Error { 25 | /// Unauthorized 26 | case unauthorized 27 | 28 | /// Non-registered route 29 | case nonRegisteredRoute 30 | 31 | /// Inactive scene 32 | case inactiveScene 33 | 34 | /// Missing parameters 35 | case missingParameters 36 | } 37 | -------------------------------------------------------------------------------- /Code/Extensions/DispatchQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | // MARK: - Main sync safe 26 | extension DispatchQueue { 27 | /// Executes given work synchronously in main thread, safely 28 | /// - Parameter work: Work to be done 29 | class func mainSyncSafe(execute work: () -> Void) { 30 | if Thread.isMainThread { 31 | work() 32 | } else { 33 | DispatchQueue.main.sync(execute: work) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Code/Extensions/SwiftUIView+TypeErasure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView+TypeErasure.swift 3 | // NavigationRouter 4 | // 5 | // Created by Cristian Ortega on 16/07/2020. 6 | // Copyright © 2020 Cristian Ortega Gómez. All rights reserved. 7 | // 8 | 9 | #if canImport(SwiftUI) 10 | import SwiftUI 11 | 12 | // MARK: - Type erasure 13 | @available(iOS 13.0, macOS 10.15, *) 14 | public extension View { 15 | /// Erases current view as AnyView 16 | /// - Returns: AnyView representing current view 17 | func eraseToAnyView() -> AnyView { 18 | AnyView(self) 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /Code/Extensions/UIWindow+Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | import UIKit 25 | 26 | // MARK: - Navigation transitions 27 | public extension UIWindow { 28 | /// Transition Options 29 | struct TransitionOptions { 30 | /// Curve of animation 31 | /// 32 | /// - linear: linear 33 | /// - easeIn: ease in 34 | /// - easeOut: ease out 35 | /// - easeInOut: ease in - ease out 36 | // swiftlint:disable:next nesting missing_docs 37 | public enum Curve { 38 | /// Linear 39 | case linear 40 | 41 | /// Ease-in 42 | case easeIn 43 | 44 | /// Ease-out 45 | case easeOut 46 | 47 | /// Ease-in-out 48 | case easeInOut 49 | 50 | /// Return the media timing function associated with curve 51 | var function: CAMediaTimingFunction { 52 | let key: String! 53 | switch self { 54 | case .linear: key = CAMediaTimingFunctionName.linear.rawValue 55 | case .easeIn: key = CAMediaTimingFunctionName.easeIn.rawValue 56 | case .easeOut: key = CAMediaTimingFunctionName.easeOut.rawValue 57 | case .easeInOut: key = CAMediaTimingFunctionName.easeInEaseOut.rawValue 58 | } 59 | return CAMediaTimingFunction(name: 60 | CAMediaTimingFunctionName(rawValue: key)) 61 | } 62 | } 63 | 64 | /// Direction of the animation 65 | /// 66 | /// - fade: fade to new controller 67 | /// - toTop: slide from bottom to top 68 | /// - toBottom: slide from top to bottom 69 | /// - toLeft: pop to left 70 | /// - toRight: push to right 71 | // swiftlint:disable:next nesting missing_docs 72 | public enum Direction { 73 | /// Fade 74 | case fade 75 | 76 | /// To top 77 | case toTop 78 | 79 | /// To bottom 80 | case toBottom 81 | 82 | /// To left 83 | case toLeft 84 | 85 | /// To right 86 | case toRight 87 | 88 | /// Return the associated transition 89 | /// 90 | /// - Returns: transition 91 | func transition() -> CATransition { 92 | let transition = CATransition() 93 | transition.type = CATransitionType.push 94 | switch self { 95 | case .fade: 96 | transition.type = CATransitionType.fade 97 | transition.subtype = nil 98 | 99 | case .toLeft: 100 | transition.subtype = CATransitionSubtype.fromLeft 101 | 102 | case .toRight: 103 | transition.subtype = CATransitionSubtype.fromRight 104 | 105 | case .toTop: 106 | transition.subtype = CATransitionSubtype.fromTop 107 | 108 | case .toBottom: 109 | transition.subtype = CATransitionSubtype.fromBottom 110 | } 111 | return transition 112 | } 113 | } 114 | 115 | /// Background of the transition 116 | /// 117 | /// - solidColor: solid color 118 | /// - customView: custom view 119 | // swiftlint:disable:next nesting missing_docs 120 | public enum Background { 121 | /// Solid color 122 | case solidColor(_: UIColor) 123 | } 124 | 125 | /// Duration of the animation (default is 0.20s) 126 | public var duration: TimeInterval = 0.20 127 | 128 | /// Direction of the transition (default is `toRight`) 129 | public var direction: TransitionOptions.Direction = .toRight 130 | 131 | /// Style of the transition (default is `linear`) 132 | public var style: TransitionOptions.Curve = .linear 133 | 134 | /// Background of the transition (default is `nil`) 135 | public var background: TransitionOptions.Background? 136 | 137 | /// Initialize a new options object with given direction and curve 138 | /// 139 | /// - Parameters: 140 | /// - direction: direction 141 | /// - style: style 142 | public init(direction: TransitionOptions.Direction = .toRight, style: TransitionOptions.Curve = .linear) { 143 | self.direction = direction 144 | self.style = style 145 | } 146 | 147 | /// Initializes a new instance 148 | public init() { } 149 | 150 | /// Return the animation to perform for given options object 151 | var animation: CATransition { 152 | let transition = self.direction.transition() 153 | transition.duration = self.duration 154 | transition.timingFunction = self.style.function 155 | return transition 156 | } 157 | } 158 | 159 | /// Change the root view controller of the window 160 | /// 161 | /// - Parameters: 162 | /// - controller: controller to set 163 | /// - options: options of the transition 164 | func setRootViewController(_ controller: UIViewController, 165 | options: TransitionOptions = TransitionOptions()) { 166 | var transitionWnd: UIWindow? 167 | if let background = options.background { 168 | transitionWnd = UIWindow(frame: UIScreen.main.bounds) 169 | switch background { 170 | case .solidColor(let color): 171 | transitionWnd?.backgroundColor = color 172 | } 173 | transitionWnd?.makeKeyAndVisible() 174 | } 175 | 176 | // Make animation 177 | self.layer.add(options.animation, forKey: kCATransition) 178 | self.rootViewController = controller 179 | self.makeKeyAndVisible() 180 | 181 | if let wnd = transitionWnd { 182 | DispatchQueue.main.asyncAfter(deadline: (.now() + 1 + options.duration), execute: { 183 | wnd.removeFromSuperview() 184 | }) 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Code/Extensions/UIWindow+KeyWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | import UIKit 25 | 26 | // MARK: - Key window access (multi OS versions support) 27 | public extension UIWindow { 28 | /// Key window 29 | static var keyWindow: UIWindow? { 30 | if #available(iOS 13.0, macOS 10.15, *) { 31 | return UIApplication.shared.connectedScenes 32 | .filter({ $0.activationState == .foregroundActive }).map({$0 as? UIWindowScene}) 33 | .compactMap({ $0 }).first?.windows.filter({ 34 | #if targetEnvironment(macCatalyst) 35 | return true 36 | #else 37 | return $0.isKeyWindow 38 | #endif 39 | }).first 40 | } else { 41 | return UIApplication.shared.keyWindow 42 | } 43 | } 44 | 45 | /// Gets window for given UIScene instance 46 | /// - Parameter scene: UIScene instance to return window for 47 | /// - Returns: UIWindow corresponding to given UIScene 48 | @available(iOS 13.0, macOS 10.15, *) 49 | static func keyWindow(forScene scene: UIScene) -> UIWindow? { 50 | return UIApplication.shared.connectedScenes 51 | .filter({ $0 == scene }).map({$0 as? UIWindowScene}) 52 | .compactMap({$0}).first?.windows.filter({ 53 | #if targetEnvironment(macCatalyst) 54 | return true 55 | #else 56 | return $0.isKeyWindow 57 | #endif 58 | }).first 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Code/Helpers/PathMatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// Checks whether a path matches another path (with optional variables) 26 | struct PathMatcher { 27 | // MARK: - Fields 28 | 29 | /// Path to be matched 30 | private let matchPath: String 31 | 32 | /// Path pattern 33 | private let pathPattern: String 34 | 35 | // MARK: - Initializers 36 | 37 | /// Initializes a new instance with given data 38 | /// - Parameters: 39 | /// - matchPath: Path to be matched 40 | /// - exact: Whether the matching must be exact or not 41 | init(match matchPath: String, exact: Bool) { 42 | // Prepare the pattern for a quick match. 43 | var newPattern = matchPath.replacingOccurrences(of: #"(:[^/?]+)"#, 44 | with: #"([^/]+)"#, 45 | options: .regularExpression) 46 | newPattern = newPattern.isEmpty ? #"\.?"# : newPattern 47 | 48 | if exact { 49 | newPattern = "^" + newPattern + "$" 50 | } 51 | 52 | self.matchPath = matchPath 53 | self.pathPattern = newPattern 54 | } 55 | 56 | /// Gets whether the path matches 57 | /// - Parameter path: Path to be matched 58 | func matches(_ path: String) -> Bool { 59 | path.range(of: pathPattern, options: .regularExpression) != nil 60 | } 61 | 62 | /// Returns a dictionary of parameter names and variables if a match was found. 63 | /// Will return `nil` otherwise. 64 | func execute(path: String) throws -> [String: String]? { 65 | guard matches(path) else { 66 | return nil 67 | } 68 | 69 | // Create and perform regex to catch parameter names. 70 | let regex = try NSRegularExpression(pattern: pathPattern, options: []) 71 | var parameterIndex: [Int: String] = [:] 72 | 73 | // Read the variable names from `matchPath`. 74 | var nsrange = NSRange(matchPath.startIndex.. 1 { 79 | if let range = Range(match.range(at: 1), in: matchPath) { 80 | parameterIndex[index] = String(matchPath[range]) 81 | } 82 | } 83 | 84 | // 85 | // Now get the variables from the given `path`. 86 | nsrange = NSRange(path.startIndex.. 1 { 94 | for a in 1.. Bool { 64 | return lhs.path == rhs.path 65 | } 66 | 67 | // MARK: - Hashable 68 | 69 | /// Hashes this instance into given hasher 70 | /// - Parameter hasher: Hasher instance 71 | public func hash(into hasher: inout Hasher) { 72 | hasher.combine(path) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Code/Modules/RoutableModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// Routable module protocol 26 | @objc public protocol RoutableModule { 27 | // MARK: - Initializers 28 | 29 | /// Initializes a new instance 30 | init() 31 | 32 | // MARK: - Setup 33 | 34 | /// Initializes module instance 35 | @objc optional func setup() 36 | 37 | // MARK: - Navigation 38 | 39 | /// Registers routes 40 | @objc optional func registerRoutes() 41 | 42 | /// Registers interceptors 43 | @objc optional func registerInterceptors() 44 | } 45 | 46 | // MARK: - Initializers 47 | public extension RoutableModule { 48 | /// Initializes a new instance 49 | init() { 50 | self.init() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Code/Modules/RoutableModulesFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// Routable modules factory 26 | public final class RoutableModulesFactory { 27 | // MARK: - Fields 28 | 29 | /// Whether routable modules have already been registered or not 30 | private static var modulesAlreadyRegistered: Bool = false 31 | 32 | // MARK: - Public methods 33 | 34 | /// Loads all modules conforming to RoutableModule protocol 35 | public static func loadRoutableModules() { 36 | // Make sure we always perform this on main thread, safely 37 | DispatchQueue.mainSyncSafe { 38 | // Ensure we register this just once to avoid duplicated stuff in the router 39 | guard !Self.modulesAlreadyRegistered else { 40 | return 41 | } 42 | 43 | // Set modules as registered 44 | Self.modulesAlreadyRegistered = true 45 | 46 | // Find all classes 47 | let featureClasses: [RoutableModule.Type] = 48 | Self.getClassesConformingProtocol(RoutableModule.self) as? 49 | [RoutableModule.Type] ?? [] 50 | 51 | // Register each feature 52 | for feature in featureClasses { 53 | let featureInstance: RoutableModule = feature.init() 54 | 55 | // Setup feature 56 | featureInstance.setup?() 57 | 58 | // Register routes 59 | featureInstance.registerRoutes?() 60 | 61 | // Register interceptors 62 | featureInstance.registerInterceptors?() 63 | } 64 | } 65 | } 66 | 67 | // MARK: - Private methods 68 | 69 | /// Gets classes conforming to given Protocol 70 | /// - Parameter p: Protocol description 71 | private static func getClassesConformingProtocol(_ protocolToConform: Protocol) -> [AnyClass] { 72 | let expectedClassCount = objc_getClassList(nil, 0) 73 | let allClasses = UnsafeMutablePointer.allocate(capacity: Int(expectedClassCount)) 74 | let autoreleasingAllClasses = AutoreleasingUnsafeMutablePointer(allClasses) 75 | let actualClassCount: Int32 = objc_getClassList(autoreleasingAllClasses, expectedClassCount) 76 | 77 | // swiftlint:disable identifier_name 78 | var classes = [AnyClass]() 79 | for i in 0 ..< actualClassCount { 80 | let currentClass: AnyClass = allClasses[Int(i)] 81 | if class_conformsToProtocol(currentClass, protocolToConform) { 82 | classes.append(currentClass) 83 | } 84 | } 85 | // swiftlint:enable identifier_name 86 | 87 | return classes 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Code/Protocols/MVVM/Router+MVVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | #if canImport(SwiftUI) 26 | import SwiftUI 27 | 28 | /// Routable view model 29 | public protocol RoutableViewModel: Routable { 30 | /// Routed view 31 | @available(iOS 13.0, macOS 10.15, *) 32 | var routedView: AnyView { get } 33 | } 34 | 35 | /// Routable view model 36 | public extension RoutableViewModel { 37 | /// Routed view 38 | @available(iOS 13.0, macOS 10.15, *) 39 | var routedView: AnyView { 40 | EmptyView() 41 | .eraseToAnyView() 42 | } 43 | 44 | /// Routed view controller 45 | @available(iOS 13.0, macOS 10.15, *) 46 | var routedViewController: UIViewController { 47 | UIHostingController(rootView: self.routedView) 48 | } 49 | } 50 | 51 | /// Routable view 52 | @available(iOS 13.0, macOS 10.15, *) 53 | public protocol RoutableView where Self: View { 54 | // MARK: - Associated types 55 | 56 | /// View model type 57 | associatedtype ViewModel: RoutableViewModel 58 | 59 | // MARK: - Fields 60 | 61 | /// View model instance 62 | var viewModel: ViewModel { get } 63 | } 64 | #endif 65 | 66 | #if canImport(UIKit) 67 | import UIKit 68 | 69 | /// Routable view controller 70 | public protocol RoutableViewController where Self: UIViewController { 71 | // MARK: - Associated types 72 | 73 | /// View model type 74 | associatedtype ViewModel: RoutableViewModel 75 | 76 | // MARK: - Fields 77 | 78 | /// View model instance 79 | var viewModel: ViewModel! { get } 80 | } 81 | #endif 82 | -------------------------------------------------------------------------------- /Code/Protocols/Routable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | /// Routable protocol 24 | public protocol Routable { 25 | // MARK: - Static fields 26 | 27 | /// Required parameters 28 | static var requiredParameters: [String]? { get } 29 | 30 | // MARK: - Fields 31 | 32 | /// Navigation interception flow (if any) 33 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? { get set } 34 | 35 | // MARK: - Initializers 36 | 37 | /// Initializes a new instance with given parameters 38 | /// - Parameter parameters: Parameters provided by router 39 | init(parameters: [String: String]?) 40 | 41 | // MARK: - View 42 | 43 | /// Routed view 44 | var routedViewController: UIViewController { get } 45 | } 46 | 47 | #if canImport(SwiftUI) 48 | import SwiftUI 49 | 50 | @available(iOS 13.0, macOS 10.15, *) 51 | public extension Routable where Self: View { 52 | /// Routed view 53 | var routedViewController: UIViewController { 54 | UIHostingController(rootView: self.eraseToAnyView()) 55 | } 56 | } 57 | #endif 58 | 59 | #if canImport(UIKit) 60 | import UIKit 61 | public extension Routable where Self: UIViewController { 62 | /// Initializes a new instance with given data 63 | /// - Parameters: 64 | /// - nibNameOrNil: Nib name (or nil) 65 | /// - nibBundleOrNil: Nib bundle (or nil) 66 | /// - parameters: Navigation parameters 67 | init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?, parameters: [String: String]?) { 68 | self.init(parameters: parameters) 69 | self.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 70 | } 71 | 72 | /// Initializes a new instance with given data 73 | /// - Parameters: 74 | /// - coder: Coder isntance 75 | /// - parameters: Navigation parameters 76 | init?(coder: NSCoder, parameters: [String: String]?) { 77 | self.init(parameters: parameters) 78 | self.init(coder: coder) 79 | } 80 | 81 | /// Routed view 82 | var routedViewController: UIViewController { 83 | self 84 | } 85 | } 86 | #endif 87 | -------------------------------------------------------------------------------- /Code/Protocols/Route.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// Route protocol 26 | protocol Route: Hashable { 27 | // MARK: - Fields 28 | 29 | /// Path 30 | var path: String { get } 31 | 32 | /// Whether the route requires authentication or not, defaults to true 33 | var requiresAuthentication: Bool { get } 34 | 35 | /// View 36 | var type: Routable.Type { get } 37 | 38 | /// Whether the route is allowed externally or not 39 | var allowedExternally: Bool { get } 40 | } 41 | -------------------------------------------------------------------------------- /Code/Protocols/Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// Router protocol 26 | public protocol Router { 27 | // MARK: - Routes 28 | 29 | /// Bind given route 30 | /// - Parameter route: Route to bind 31 | static func bind(route: NavigationRoute) 32 | 33 | /// Bind given routes 34 | /// - Parameter routes: Routes to bind 35 | static func bind(routes: [NavigationRoute]) 36 | 37 | /// Unbind given route 38 | /// - Parameter route: Route to unbind 39 | static func unbind(route: NavigationRoute) 40 | 41 | /// Unbind given routes 42 | /// - Parameter routes: Routes to unbind 43 | static func unbind(routes: [NavigationRoute]) 44 | 45 | // MARK: - Interception 46 | 47 | /// Intercepts navigation for specified path 48 | /// - Parameters: 49 | /// - interceptedPath: Path to intercept 50 | /// - when: When to intercept navigation 51 | /// - priority: Interception priority 52 | /// - isAuthenticationRequired: Whether the interception requires authentication or not 53 | /// - handler: Interception handler 54 | static func interceptNavigation( 55 | toPath interceptedPath: String, 56 | when: NavigationInterceptorPoint, 57 | withPriority priority: NavigationInterceptionPriority, 58 | isAuthenticationRequired requiresAuthentication: Bool, 59 | handler: ((NavigationRouter, ((Bool?) -> Void)?) -> Void)?) 60 | 61 | // MARK: - Navigation 62 | 63 | /// Navigates to given path with given data 64 | /// - Parameters: 65 | /// - path: Path to navigate to 66 | /// - replace: Whether to replace navigation stack or not 67 | /// - externally: Whether the navigation is from an external source or not 68 | /// - embedInNavigationView: Whether to embed the destination view into a UINavigationController instance or not 69 | /// - modal: Whether to show destination view as modal or not 70 | /// - shouldPreventDismissal: Whether the presented modal should prevent dismissal or not 71 | /// - interceptionExecutionFlow: Navigation interception execution flow 72 | /// - animation: Navigation animation for stack replacing (if any) 73 | func navigate( 74 | toPath path: String, 75 | replace: Bool, 76 | externally: Bool, 77 | embedInNavigationView: Bool, 78 | modal: Bool, 79 | shouldPreventDismissal: Bool, 80 | interceptionExecutionFlow: NavigationInterceptionFlow?, 81 | animation: NavigationTransition?) 82 | } 83 | -------------------------------------------------------------------------------- /Code/Protocols/RouterAuthenticationHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// Router authentication handler 26 | @objc public protocol RouterAuthenticationHandler { 27 | // MARK: - Authentication 28 | 29 | /// Gets whether user is authenticated or not 30 | var isAuthenticated: Bool { get } 31 | 32 | /// Logins user 33 | /// - Parameter completion: Completion handler 34 | func login(completion: (() -> Void)?) 35 | 36 | /// Logouts user 37 | /// - Parameter completion: Completion handler 38 | func logout(completion: (() -> Void)?) 39 | 40 | /// Gets whether authentication handler can handle given callback URL or not 41 | /// - Parameter url: URL to be handled 42 | @objc optional func canHandleCallbackUrl(_ url: URL) -> Bool 43 | 44 | /// Handles given callback URL 45 | /// - Parameter url: URL to be handled 46 | @objc optional func handleCallbackUrl(_ url: URL) 47 | } 48 | -------------------------------------------------------------------------------- /Code/Protocols/RouterErrorHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | /// Error handler interface for RouterErrorHandler 24 | public protocol RouterErrorHandler { 25 | /// Handles given error 26 | /// - Parameter error: Routing error to be handled 27 | func handleError(_ error: RoutingError) 28 | } 29 | -------------------------------------------------------------------------------- /Code/Routing/Extensions/NavigationRouter+ErrorHandling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | // MARK: - Error handling 26 | extension NavigationRouter { 27 | /// Handles given error 28 | /// - Parameter error: Error to be handled 29 | func handleError(forPath path: String, _ error: RoutingError) { 30 | Self.errorHandler?.handleError(error) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Code/Routing/Extensions/NavigationRouter+NavigationHandling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | import UIKit 25 | #if canImport(SwiftUI) 26 | import SwiftUI 27 | #endif 28 | 29 | // MARK: - Navigation handling 30 | extension NavigationRouter { 31 | /// Navigates to given path 32 | /// - Parameters: 33 | /// - path: Path to navigate to 34 | /// - replace: Whether to replace the stack or not 35 | /// - externally: Whether the navigation was launched externally or not. Defaults to false. 36 | /// - embedInNavigationView: Whether to embed the view in a NavigationView or not 37 | /// - modal: Whether to present navigation as modal or not 38 | /// - shouldPreventDismissal: Whether modal dismissal should be prevented or not 39 | /// - interceptionExecutionFlow: Navigation interception execution flow (if any) 40 | /// - animation: Animation to use for navigation 41 | open func navigate( 42 | toPath path: String, 43 | replace: Bool = false, 44 | externally: Bool = false, 45 | embedInNavigationView: Bool = true, 46 | modal: Bool = false, 47 | shouldPreventDismissal: Bool = false, 48 | interceptionExecutionFlow: NavigationInterceptionFlow? = nil, 49 | animation: NavigationTransition? = nil) { 50 | 51 | self.dispatchQueue.async { 52 | self.checkNavigationRequirementsAndNavigate(toPath: path, 53 | replace: replace, 54 | externally: externally, 55 | embedInNavigationView: embedInNavigationView, 56 | modal: modal, 57 | shouldPreventDismissal: shouldPreventDismissal, 58 | interceptionExecutionFlow: interceptionExecutionFlow, 59 | animation: animation) 60 | } 61 | } 62 | 63 | /// Whether the router can navigate to a given path or not 64 | /// - Parameter path: Path to navigate to 65 | /// - Parameter externally: Whether the navigation is coming externally or not 66 | open func canNavigate(toPath path: String, 67 | externally: Bool = false) -> Bool { 68 | // Check if it is an external url and let the system handle it 69 | guard path.starts(with: "/") else { 70 | if let url: URL = URL(string: path), UIApplication.shared.canOpenURL(url) { 71 | return true 72 | } 73 | return false 74 | } 75 | 76 | // Check for a route matching given path 77 | guard let route: NavigationRoute = Self.routes.first(where: { 78 | self.path(path.lowercased(), matchesRoutePath: $0.path.lowercased()) 79 | }) else { 80 | // Let the authentication handler handle callback URL if applicable 81 | if let callbackUrl: URL = URL(string: path), 82 | Self.authenticationHandler?.canHandleCallbackUrl?(callbackUrl) ?? false { 83 | return true 84 | } 85 | 86 | // Non-registered route and given path is not the callback URL for authorization 87 | return false 88 | } 89 | 90 | // Ensure route can be launched externally if it is coming from a deeplink 91 | guard !externally || route.allowedExternally else { 92 | // Do nothing, external navigation not allowed for given path 93 | return false 94 | } 95 | 96 | // Ensure authentication is available 97 | guard !route.requiresAuthentication || Self.authenticationHandler != nil else { 98 | return false 99 | } 100 | 101 | return true 102 | } 103 | 104 | /// Gets view controller for given path 105 | /// - Parameter path: Path to return view for 106 | /// - Returns: UIViewController 107 | open func viewControllerFor(path: String) -> UIViewController? { 108 | // Get route 109 | guard let route: NavigationRoute = Self.routes.first(where: { 110 | self.path(path.lowercased(), matchesRoutePath: $0.path.lowercased()) 111 | }) else { 112 | return nil 113 | } 114 | 115 | // Parse parameters 116 | let parameters: [String: String]? = self.path(path, toDictionaryForRoutePath: route.path) 117 | 118 | // Ensure we've got valid parameters 119 | if !(route.type.requiredParameters?.isEmpty ?? true) { 120 | guard parameters != nil else { 121 | return nil 122 | } 123 | let givenParametersNames: Set = Set(parameters!.keys) 124 | 125 | // Ensure parameters matches required parameters by view model 126 | let requiredParametersNames: [String] = route.type.requiredParameters ?? [] 127 | for requiredParameter in requiredParametersNames { 128 | if !givenParametersNames.contains(requiredParameter) { 129 | return nil 130 | } 131 | } 132 | } 133 | 134 | // Instantiate routable 135 | let routable: Routable = route.type.init(parameters: parameters) 136 | 137 | // Return view controller 138 | return routable.routedViewController 139 | } 140 | 141 | #if canImport(SwiftUI) 142 | /// Gets view for given path 143 | /// - Parameter path: Path to return view for 144 | /// - Returns: UIViewController 145 | @available(iOS 13.0, macOS 10.15, *) 146 | open func viewFor(path: String) -> AnyView { 147 | let defaultView: AnyView = EmptyView().eraseToAnyView() 148 | 149 | // Get route 150 | guard let route: NavigationRoute = Self.routes.first(where: { 151 | self.path(path.lowercased(), matchesRoutePath: $0.path.lowercased()) 152 | }) else { 153 | return defaultView 154 | } 155 | 156 | // Parse parameters 157 | let parameters: [String: String]? = self.path(path, toDictionaryForRoutePath: route.path) 158 | 159 | // Ensure we've got valid parameters 160 | if !(route.type.requiredParameters?.isEmpty ?? true) { 161 | guard parameters != nil else { 162 | return defaultView 163 | } 164 | let givenParametersNames: Set = Set(parameters!.keys) 165 | 166 | // Ensure parameters matches required parameters by view model 167 | let requiredParametersNames: [String] = route.type.requiredParameters ?? [] 168 | for requiredParameter in requiredParametersNames { 169 | if !givenParametersNames.contains(requiredParameter) { 170 | return defaultView 171 | } 172 | } 173 | } 174 | 175 | // Instantiate routable 176 | let routable: Routable = route.type.init(parameters: parameters) 177 | guard let hostingController: UIHostingController = 178 | routable.routedViewController as? UIHostingController else { 179 | return defaultView 180 | } 181 | 182 | // Return view 183 | return hostingController.rootView 184 | } 185 | #endif 186 | 187 | /// Dismisses modal if needed 188 | open func dismissModalIfNeeded() { 189 | DispatchQueue.main.async { 190 | // Get root controller from active scene 191 | guard let keyWindow: UIWindow = self.keyWindow, 192 | let rootViewController = keyWindow.rootViewController else { 193 | return 194 | } 195 | 196 | rootViewController.presentedViewController?.dismiss(animated: true, completion: nil) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Code/Routing/Extensions/NavigationRouter+NavigationInterception.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// Navigation interception enum 26 | public enum NavigationInterceptorPoint: Int, Codable { 27 | /// Before navigating 28 | case before 29 | 30 | /// After navigating 31 | case after 32 | } 33 | 34 | /// Navigation interception priority 35 | public enum NavigationInterceptionPriority: Int, Codable, Comparable { 36 | /// Low priority 37 | case low 38 | 39 | /// Medium priority 40 | case medium 41 | 42 | /// High priority 43 | case high 44 | 45 | /// Mandatory priority 46 | case mandatory 47 | 48 | // MARK: - Comparable 49 | 50 | /// Compares two given instances 51 | /// - Parameters: 52 | /// - lhs: First instance to compare 53 | /// - rhs: Second instance to compare 54 | public static func < (lhs: NavigationInterceptionPriority, rhs: NavigationInterceptionPriority) -> Bool { 55 | return lhs.rawValue < rhs.rawValue 56 | } 57 | } 58 | 59 | /// Navigation interceptor 60 | struct NavigationInterceptor: Comparable { 61 | // MARK: - Fields 62 | 63 | /// Path 64 | var path: String 65 | 66 | /// Navigation interceptor point 67 | var when: NavigationInterceptorPoint 68 | 69 | /// Priority 70 | var priority: NavigationInterceptionPriority 71 | 72 | /// Whether the interceptor requires authentication or not 73 | var requiresAuthentication: Bool 74 | 75 | /// Handler 76 | var handler: ((NavigationRouter, ((Bool?) -> Void)?) -> Void)? 77 | 78 | // MARK: - Comparable 79 | 80 | /// Compares given instances 81 | /// - Parameters: 82 | /// - lhs: First instance 83 | /// - rhs:Second instance 84 | static func < (lhs: NavigationInterceptor, rhs: NavigationInterceptor) -> Bool { 85 | // Order by priority 86 | if lhs.priority > rhs.priority { 87 | return false 88 | } 89 | 90 | // Order by authentication 91 | if !lhs.requiresAuthentication && rhs.requiresAuthentication { 92 | return false 93 | } 94 | 95 | // In any other case, lhs < rhs 96 | return true 97 | } 98 | 99 | /// Gets whether two given instances are equal or not 100 | /// - Parameters: 101 | /// - lhs: First instance 102 | /// - rhs: Second instance 103 | static func == (lhs: NavigationInterceptor, rhs: NavigationInterceptor) -> Bool { 104 | return lhs.path.lowercased() == rhs.path.lowercased() && 105 | lhs.when == rhs.when && 106 | lhs.priority == rhs.priority && 107 | lhs.requiresAuthentication == rhs.requiresAuthentication 108 | } 109 | } 110 | 111 | /// Navigation interception flow 112 | public struct NavigationInterceptionFlow { 113 | // MARK: - Fields 114 | 115 | /// Completion handler to continue flow execution (if any) 116 | public var `continue`: ((Bool?) -> Void)? 117 | 118 | // MARK: - Initializers 119 | 120 | /// Initializes a new instance with given completion handler 121 | /// - Parameter completion: Completion handler to continue execution (if any) 122 | public init(completion: ((Bool?) -> Void)?) { 123 | self.continue = completion 124 | } 125 | } 126 | 127 | // MARK: - Navigation interception 128 | extension NavigationRouter { 129 | // MARK: Interceptors 130 | 131 | /// Interceps navigation to given path with given handler 132 | /// - Parameters: 133 | /// - interceptedPath: Path to intercept navigation for 134 | /// - when: When to inercept navigation 135 | /// - priority: Interception priority 136 | /// - isAuthenticationRequired: Whether the interception requires authentication or not 137 | /// - handler: Interception handler 138 | public static func interceptNavigation( 139 | toPath interceptedPath: String, 140 | when: NavigationInterceptorPoint = .before, 141 | withPriority priority: NavigationInterceptionPriority = .low, 142 | isAuthenticationRequired requiresAuthentication: Bool = false, 143 | handler: ((NavigationRouter, ((Bool?) -> Void)?) -> Void)?) { 144 | // Add interceptor 145 | self.interceptors.append(NavigationInterceptor( 146 | path: interceptedPath, 147 | when: when, 148 | priority: priority, 149 | requiresAuthentication: requiresAuthentication, 150 | handler: handler)) 151 | } 152 | 153 | /// Removes all registered interceptors for given path 154 | /// - Parameter path: Path to remove interceptors for 155 | static func removeInterceptors(forPath path: String) { 156 | self.interceptors.removeAll(where: { 157 | $0.path.lowercased() == path.lowercased() 158 | }) 159 | } 160 | 161 | // MARK: Event handlers 162 | 163 | /// Router will navigate handler 164 | /// - Parameter route: Route the router will navigate to 165 | /// - Parameter parameters: Parameters used for navigation 166 | /// - Parameter originalPath: Original navigation path 167 | /// - Parameter replace: Whether to replace the stack or not 168 | /// - Parameter externally: Whether to navigate externally or not 169 | /// - Parameter embedInNavigationView: Whether to embed destination view in a navigation view or not 170 | /// - Parameter modal: Whether to present the destination view as modal or not 171 | /// - Parameter shouldPreventDismissal: Whether the presented modal should prevent dismissal or not (if applicable) 172 | /// - Parameter animation: Navigation transition 173 | /// - Parameter completion: Completion handler 174 | func routerWillNavigate( 175 | toRoute route: NavigationRoute, 176 | withParameters parameters: [String: String]?, 177 | originalPath: String, 178 | replace: Bool = false, 179 | embedInNavigationView: Bool = true, 180 | modal: Bool = false, 181 | shouldPreventDismissal: Bool = false, 182 | animation: NavigationTransition? = nil, 183 | completion: @escaping (() -> Void)) { 184 | // Get interceptors for given path 185 | let interceptors: [NavigationInterceptor] = Self.interceptors 186 | .filter({ $0.path.lowercased() == route.path.lowercased() && $0.when == .before }) 187 | guard !interceptors.isEmpty else { 188 | DispatchQueue.main.async { 189 | completion() 190 | } 191 | return 192 | } 193 | 194 | // Sort interceptors by priority 195 | let sortedInterceptors: [NavigationInterceptor] = interceptors.sorted(by: { $0 > $1 }) 196 | 197 | // Execute each interceptor by priority and let them handle their own code asynchronously 198 | self.handleInterceptors( 199 | sortedInterceptors, 200 | when: .before, 201 | forRoute: route, 202 | withParameters: parameters, 203 | originalPath: originalPath, 204 | replace: replace, 205 | embedInNavigationView: embedInNavigationView, 206 | modal: modal, 207 | shouldPreventDismissal: shouldPreventDismissal, 208 | animation: animation, 209 | completion: completion) 210 | } 211 | 212 | /// Router did navigate handler 213 | /// - Parameter route: Route the router will navigate to 214 | /// - Parameter parameters: Parameters used for navigation 215 | /// - Parameter originalPath: Original navigation path 216 | /// - Parameter replace: Whether to replace the stack or not 217 | /// - Parameter externally: Whether to navigate externally or not 218 | /// - Parameter embedInNavigationView: Whether to embed destination view in a navigation view or not 219 | /// - Parameter modal: Whether to present the destination view as modal or not 220 | /// - Parameter shouldPreventDismissal: Whether the presented modal should prevent dismissal or not (if applicable) 221 | /// - Parameter animation: Transition animation (if any) 222 | func routerDidNavigate( 223 | toRoute route: NavigationRoute, 224 | withParameters parameters: [String: String]?, 225 | originalPath: String, 226 | replace: Bool = false, 227 | embedInNavigationView: Bool = true, 228 | modal: Bool = false, 229 | shouldPreventDismissal: Bool = false, 230 | animation: NavigationTransition? = nil) { 231 | // Get interceptors for given path 232 | let interceptors: [NavigationInterceptor] = Self.interceptors 233 | .filter({ $0.path.lowercased() == route.path.lowercased() && $0.when == .after }) 234 | guard !interceptors.isEmpty else { 235 | return 236 | } 237 | 238 | // Sort interceptors by priority 239 | let sortedInterceptors: [NavigationInterceptor] = interceptors.sorted(by: { $0 > $1 }) 240 | 241 | // Execute each interceptor by priority and let them handle their own code asynchronously 242 | self.handleInterceptors( 243 | sortedInterceptors, 244 | when: .after, 245 | forRoute: route, 246 | withParameters: parameters, 247 | originalPath: originalPath, 248 | replace: replace, 249 | embedInNavigationView: embedInNavigationView, 250 | modal: modal, 251 | shouldPreventDismissal: shouldPreventDismissal, 252 | animation: animation) 253 | } 254 | 255 | /// Handles given interceptors 256 | /// - Parameters: 257 | /// - interceptors: Interceptors to be handled 258 | /// - route: Route to navigate to 259 | /// - parameters: Parameters to use for navigation 260 | /// - originalPath: Original navigation path 261 | /// - replace: Whether to replace the stack with the destination view or not 262 | /// - externally: Whether to navigate externally or not 263 | /// - embedInNavigationView: Whether to embed the destination view in a navigation view or not 264 | /// - modal: Whether to use a modal presentation style for the destination view or not 265 | /// - shouldPreventDismissal: Whether the presented modal (if applicable) should prevent dismissal or not 266 | /// - when: Interception point 267 | /// - animation: Transition animation (if any) 268 | /// - completion: Completion handler (if any) 269 | private func handleInterceptors( 270 | _ interceptors: [NavigationInterceptor], 271 | when: NavigationInterceptorPoint, 272 | forRoute route: NavigationRoute, 273 | withParameters parameters: [String: String]?, 274 | originalPath: String, 275 | replace: Bool = false, 276 | embedInNavigationView: Bool = true, 277 | modal: Bool = false, 278 | shouldPreventDismissal: Bool = false, 279 | animation: NavigationTransition? = nil, 280 | completion: (() -> Void)? = nil) { 281 | 282 | // Get interceptor to handle 283 | guard let interceptorToHandle: NavigationInterceptor = interceptors.first else { 284 | return 285 | } 286 | 287 | // Declare interception completion handler 288 | let interceptionCompletionHandler: ((Bool?) -> Void) = { (originalNavigationMustBeCancelled: Bool?) in 289 | // Check if handled interceptor was the last one (or the only one) 290 | if interceptors.count == 1 { 291 | // Ensure original navigation must not be cancelled (if needed) 292 | if when == .before && !(originalNavigationMustBeCancelled ?? false) { 293 | // Perform original navigation 294 | DispatchQueue.main.async { 295 | completion?() 296 | } 297 | } 298 | } else if !(originalNavigationMustBeCancelled ?? false) { 299 | // Let the next interceptor handle its own code before actually navigating to the original path 300 | self.handleInterceptors(Array( 301 | interceptors.dropFirst()), 302 | when: when, 303 | forRoute: route, 304 | withParameters: parameters, 305 | originalPath: originalPath, 306 | replace: replace, 307 | embedInNavigationView: embedInNavigationView, 308 | modal: modal, 309 | shouldPreventDismissal: shouldPreventDismissal, 310 | animation: animation, 311 | completion: completion 312 | ) 313 | } 314 | 315 | // Otherwise we do not need to do anything since the original navigation has been cancelled at some point 316 | } 317 | 318 | // Check if interceptor requires authentication 319 | if interceptorToHandle.requiresAuthentication, 320 | !(Self.authenticationHandler?.isAuthenticated ?? false) { 321 | Self.authenticationHandler?.login(completion: { 322 | self.executeInterceptor(interceptorToHandle, completion: interceptionCompletionHandler) 323 | }) 324 | } else { 325 | self.executeInterceptor(interceptorToHandle, completion: interceptionCompletionHandler) 326 | } 327 | } 328 | 329 | /// Executes interceptor 330 | private func executeInterceptor( 331 | _ interceptor: NavigationInterceptor, 332 | completion: ((Bool?) -> Void)?) { 333 | DispatchQueue.main.async { 334 | interceptor.handler?(self, completion) 335 | } 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /Code/Routing/Extensions/NavigationRouter+PathMatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | // MARK: - Path matcher 26 | extension NavigationRouter { 27 | /// Gets whether given path matches given route path 28 | /// - Parameters: 29 | /// - path: Path to be compared 30 | /// - routePath: Route path 31 | func path(_ path: String, matchesRoutePath routePath: String) -> Bool { 32 | // Create patch matcher instance 33 | let pathMatcher: PathMatcher = PathMatcher(match: routePath, exact: true) 34 | 35 | // Invoke matching method 36 | return pathMatcher.matches(path) 37 | } 38 | 39 | /// Gets dictionary parameters from given path 40 | /// - Parameters: 41 | /// - path: Path 42 | /// - toDictionaryForRoutePath: Route path 43 | func path(_ path: String, toDictionaryForRoutePath routePath: String) -> [String: String]? { 44 | // Make sure route matches 45 | guard self.path(path, matchesRoutePath: routePath) else { 46 | return nil 47 | } 48 | 49 | // Instantiate matcher 50 | let pathMatcher: PathMatcher = PathMatcher(match: routePath, exact: true) 51 | 52 | // Parse parameters 53 | return try? pathMatcher.execute(path: path) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Code/Routing/NavigationRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | /// Navigation router 26 | open class NavigationRouter: Router { 27 | // MARK: - Fields 28 | 29 | // MARK: Static fields 30 | 31 | /// Main navigation router 32 | public static let main: NavigationRouter = NavigationRouter() 33 | 34 | /// Authentication handler 35 | public static var authenticationHandler: RouterAuthenticationHandler? 36 | 37 | /// Error handler 38 | public static var errorHandler: RouterErrorHandler? 39 | 40 | /// External navigation delay 41 | static var externalNavigationDelay: TimeInterval = 1 42 | 43 | /// Registered routes 44 | static var routes: Set = [] 45 | 46 | /// Interceptors array 47 | static var interceptors: [NavigationInterceptor] = [] 48 | 49 | /// Gets whether user is authenticated or not 50 | static var isUserAuthenticated: Bool { 51 | // Defaults to true 52 | return authenticationHandler?.isAuthenticated ?? true 53 | } 54 | 55 | // MARK: Instance fields 56 | 57 | /// Dispatch queue for background operations 58 | let dispatchQueue: DispatchQueue 59 | 60 | #if canImport(SwiftUI) 61 | /// Associated scene 62 | @available(iOS 13.0, macOS 10.15, *) 63 | private(set) lazy var scene: UIScene? = nil 64 | #endif 65 | 66 | /// Key window for associated scene (if any) 67 | var keyWindow: UIWindow? { 68 | if #available(iOS 13.0, macOS 10.15, *), let scene: UIScene = scene { 69 | return UIWindow.keyWindow(forScene: scene) 70 | } else { 71 | return UIWindow.keyWindow // first active scene 72 | } 73 | } 74 | 75 | // MARK: - Initializers 76 | 77 | /// Initialializes a new instance with key window 78 | private init() { 79 | self.dispatchQueue = DispatchQueue( 80 | label: "NavigationRouter-\(UUID().uuidString)", 81 | qos: .userInitiated, 82 | attributes: .concurrent, 83 | autoreleaseFrequency: .inherit, 84 | target: .global()) 85 | } 86 | 87 | /// Initializes a new instance with given scene 88 | /// - Parameter scene: UIScene instance to use router for 89 | @available(iOS 13.0, macOS 10.15, *) 90 | convenience init(scene: UIScene) { 91 | self.init() 92 | 93 | self.scene = scene 94 | } 95 | 96 | // MARK: - Static methods 97 | 98 | // MARK: Navigation binding 99 | 100 | /// Binds given routes 101 | /// - Parameter routes: Routes to be registered 102 | public static func bind(routes: [NavigationRoute]) { 103 | for route in routes { 104 | Self.bind(route: route) 105 | } 106 | } 107 | 108 | /// Binds given route 109 | /// - Parameter route: Route to be registered 110 | public static func bind(route: NavigationRoute) { 111 | // Ensure route is not already registered 112 | guard !Self.routes.contains(route) else { 113 | return 114 | } 115 | 116 | // Register route 117 | _ = Self.routes.insert(route) 118 | } 119 | 120 | /// Unbind given routes 121 | /// - Parameter routes: Routes to be unregistered 122 | public static func unbind(routes: [NavigationRoute]) { 123 | for route in routes { 124 | Self.removeInterceptors(forPath: route.path) 125 | Self.unbind(route: route) 126 | } 127 | } 128 | 129 | /// Unbinds given route 130 | /// - Parameter route: Route to be unregistered 131 | public static func unbind(route: NavigationRoute) { 132 | Self.routes.remove(route) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Code/Views/RoutedLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if canImport(SwiftUI) 24 | import SwiftUI 25 | #endif 26 | 27 | /// Routed link 28 | @available(iOS 13.0, macOS 10.15, *) 29 | public struct RoutedLink: SwiftUI.View { 30 | // MARK: - Fields 31 | 32 | /// Navigation Router 33 | private let router: Router 34 | 35 | /// Path to navigate to 36 | private let path: String 37 | 38 | /// Whether to replace navigation stack upon navigation or not 39 | private let replace: Bool 40 | 41 | /// Whether to embed destination path in a navigation view or not 42 | private let embedInNavigationView: Bool 43 | 44 | /// Whether to use a modal presentation style for destination view or not 45 | private let modal: Bool 46 | 47 | /// Whether to prevent modal dismissal or not 48 | private let shouldPreventDismissal: Bool 49 | 50 | /// Navigation interception execution flow (if any) 51 | private let interceptionExecutionFlow: NavigationInterceptionFlow? 52 | 53 | /// Transition animation 54 | private let animation: NavigationTransition? 55 | 56 | /// View contents 57 | private let label: Label 58 | 59 | // MARK: - Initializers 60 | 61 | /// Initializes a new instance for given path 62 | /// - Parameters: 63 | /// - path: Path to navigate to 64 | /// - replace: Whether to replace navigation stack or not 65 | /// - embedInNavigationView: Whether to embed destination view in a navigation view or not 66 | /// - modal: Whether to use modal presentation style for destination view or not 67 | /// - shouldPreventDismissal: Whether the presented modal should prevent dismissal or not 68 | /// - interceptionExecutionFlow: Navigation interception execution flow (if any) 69 | /// - animation: Navigation transition (if any) 70 | /// - router: Router (if any) 71 | /// - label: View contents 72 | public init(to path: String, 73 | replace: Bool = false, 74 | embedInNavigationView: Bool = true, 75 | modal: Bool = false, 76 | shouldPreventDismissal: Bool = false, 77 | interceptionExecutionFlow: NavigationInterceptionFlow? = nil, 78 | animation: NavigationTransition? = nil, 79 | router: Router = NavigationRouter.main, 80 | @ViewBuilder label: () -> Label) { 81 | self.path = path 82 | self.replace = replace 83 | self.embedInNavigationView = embedInNavigationView 84 | self.modal = modal 85 | self.shouldPreventDismissal = shouldPreventDismissal 86 | self.interceptionExecutionFlow = interceptionExecutionFlow 87 | self.animation = animation 88 | self.router = router 89 | self.label = label() 90 | } 91 | 92 | // MARK: - Body builder 93 | 94 | /// View body 95 | public var body: some SwiftUI.View { 96 | label.onTapGesture { 97 | // Navigate to given path 98 | self.router.navigate( 99 | toPath: self.path, 100 | replace: self.replace, 101 | externally: false, 102 | embedInNavigationView: self.embedInNavigationView, 103 | modal: self.modal, 104 | shouldPreventDismissal: self.shouldPreventDismissal, 105 | interceptionExecutionFlow: self.interceptionExecutionFlow, 106 | animation: self.animation) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cristian Ortega Gómez 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. 22 | -------------------------------------------------------------------------------- /NavigationRouter.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'NavigationRouter' 3 | s.version = '1.0.3' 4 | s.summary = 'A router implementation designed for complex modular apps, written in Swift' 5 | s.description = <<-DESC 6 | NavigationRouter is a router implementation designed for complex modular apps, written in Swift. 7 | DESC 8 | s.homepage = 'https://github.com/corteggo/NavigationRouter' 9 | s.license = { :type => 'MIT', :file => 'LICENSE' } 10 | s.author = { 'corteggo' => 'cristian.ortega@outlook.es' } 11 | s.source = { :git => 'https://github.com/corteggo/NavigationRouter.git', :tag => s.version.to_s } 12 | s.social_media_url = 'https://twitter.com/corteggo' 13 | s.ios.deployment_target = '11.0' 14 | s.macos.deployment_target = '10.15' 15 | s.swift_version = '5.0' 16 | s.source_files = 'Code/**/*' 17 | end 18 | -------------------------------------------------------------------------------- /NavigationRouter.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NavigationRouter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NavigationRouter.xcodeproj/xcshareddata/xcschemes/NavigationRouter.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /NavigationRouter.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /NavigationRouter.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "NavigationRouter", 6 | platforms: [ 7 | .iOS(.v11), 8 | .macOS(.v10_15) 9 | ], 10 | products: [ 11 | .library(name: "NavigationRouter", 12 | targets: ["NavigationRouter"]) 13 | ], 14 | targets: [ 15 | .target( 16 | name: "NavigationRouter", 17 | path: "Code" 18 | ) 19 | ], 20 | swiftLanguageVersions: [.v5] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NavigationRouter 2 | 3 | NavigationRouter is a router implementation designed for complex modular apps, written in Swift. 4 | 5 | ⚠️ This project is no longer maintained. Please migrate to other alternatives, such as https://github.com/pointfreeco/swiftui-navigation or use native navigation of SwiftUI instead. 6 | -------------------------------------------------------------------------------- /Supporting files/NavigationRouter-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /TestApp/NavigationRouterTestApp.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TestApp/Supporting files/TestApp-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleTypeRole 23 | None 24 | CFBundleURLSchemes 25 | 26 | routertestapp 27 | 28 | 29 | 30 | CFBundleVersion 31 | 1 32 | LSRequiresIPhoneOS 33 | 34 | UIApplicationSceneManifest 35 | 36 | UIApplicationSupportsMultipleScenes 37 | 38 | UISceneConfigurations 39 | 40 | UIWindowSceneSessionRoleApplication 41 | 42 | 43 | UISceneConfigurationName 44 | Default Configuration 45 | UISceneDelegateClassName 46 | $(PRODUCT_MODULE_NAME).SceneDelegate 47 | 48 | 49 | 50 | 51 | UILaunchStoryboardName 52 | LaunchScreen 53 | UIRequiredDeviceCapabilities 54 | 55 | armv7 56 | 57 | UISupportedInterfaceOrientations 58 | 59 | UIInterfaceOrientationPortrait 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | UISupportedInterfaceOrientations~ipad 64 | 65 | UIInterfaceOrientationPortrait 66 | UIInterfaceOrientationPortraitUpsideDown 67 | UIInterfaceOrientationLandscapeLeft 68 | UIInterfaceOrientationLandscapeRight 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /TestApp/Supporting files/TestAppTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TestApp/Supporting files/TestAppUITests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TestApp/TestApp.xcodeproj/xcshareddata/xcschemes/NavigationRouterTestApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 54 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 91 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /TestApp/TestApp/Delegates/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | import NavigationRouter 25 | 26 | @UIApplicationMain 27 | class AppDelegate: UIResponder, UIApplicationDelegate { 28 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 29 | // Prefer large titles 30 | UINavigationBar.appearance().prefersLargeTitles = true 31 | 32 | // Register modules 33 | RoutableModulesFactory.loadRoutableModules() 34 | 35 | // Override point for customization after application launch. 36 | return true 37 | } 38 | 39 | // MARK: UISceneSession Lifecycle 40 | 41 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 42 | // Called when a new scene session is being created. 43 | // Use this method to select a configuration to create the new scene with. 44 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /TestApp/TestApp/Delegates/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | import NavigationRouter 25 | 26 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 27 | class MockedAuthenticationHandler: RouterAuthenticationHandler { 28 | var authenticationRequested: Bool = false 29 | 30 | var isAuthenticated: Bool { 31 | return authenticationRequested 32 | } 33 | 34 | func login(completion: (() -> Void)?) { 35 | DispatchQueue.main.async { 36 | let alert: UIAlertController = UIAlertController(title: "Login", message: "Login successful", preferredStyle: .alert) 37 | alert.addAction(.init(title: "OK", style: .default, handler: { _ in 38 | self.authenticationRequested = true 39 | DispatchQueue.global().async { 40 | completion?() 41 | } 42 | })) 43 | 44 | guard let keyWindow: UIWindow = UIApplication.shared.connectedScenes 45 | .filter({$0.activationState == .foregroundActive}) 46 | .map({$0 as? UIWindowScene}) 47 | .compactMap({$0}) 48 | .first?.windows 49 | .filter({$0.isKeyWindow}).first, 50 | let rootViewController = keyWindow.rootViewController else { 51 | // TODO: Handle error here 52 | return 53 | } 54 | rootViewController.present(alert, animated: true, completion: nil) 55 | } 56 | } 57 | 58 | func logout(completion: (() -> Void)?) { 59 | 60 | } 61 | 62 | func canHandleCallbackUrl(_ url: URL) -> Bool { 63 | return false 64 | } 65 | 66 | func handleCallbackUrl(_ url: URL) { 67 | 68 | } 69 | } 70 | 71 | class MockedErrorHandler: RouterErrorHandler { 72 | func handleError(_ error: RoutingError) { 73 | DispatchQueue.main.async { 74 | let alert: UIAlertController = UIAlertController(title: "Error", message: "Navigation error", preferredStyle: .alert) 75 | alert.addAction(.init(title: "OK", style: .default, handler: nil)) 76 | 77 | guard let keyWindow: UIWindow = UIApplication.shared.connectedScenes 78 | .filter({$0.activationState == .foregroundActive}) 79 | .map({$0 as? UIWindowScene}) 80 | .compactMap({$0}) 81 | .first?.windows 82 | .filter({$0.isKeyWindow}).first, 83 | let rootViewController = keyWindow.rootViewController else { 84 | // TODO: Handle error here 85 | return 86 | } 87 | rootViewController.present(alert, animated: true, completion: nil) 88 | } 89 | } 90 | } 91 | 92 | var window: UIWindow? 93 | 94 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 95 | // Create mocked authentication handler for router 96 | NavigationRouter.authenticationHandler = MockedAuthenticationHandler() 97 | NavigationRouter.errorHandler = MockedErrorHandler() 98 | 99 | // Create the SwiftUI view that provides the window contents. 100 | guard let contentView: UIViewController = NavigationRouter.main.viewControllerFor(path: "/view1A") else { 101 | return 102 | } 103 | 104 | // Use a UIHostingController as window root view controller. 105 | if let windowScene = scene as? UIWindowScene { 106 | let window = UIWindow(windowScene: windowScene) 107 | let navigationController: UINavigationController = UINavigationController(rootViewController: contentView) 108 | navigationController.navigationBar.prefersLargeTitles = true 109 | window.rootViewController = navigationController 110 | self.window = window 111 | window.makeKeyAndVisible() 112 | } 113 | } 114 | 115 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { 116 | // Ensure we've got a valid URL 117 | guard let url = URLContexts.first?.url else { 118 | return 119 | } 120 | 121 | // Let the router handle external navigations 122 | if url.scheme == "routertestapp", url.relativePath.starts(with: "/navigate") { 123 | DispatchQueue.global().async { 124 | NavigationRouter.main.navigate(toPath: url.relativePath.replacingOccurrences(of: "/navigate", with: ""), externally: true) 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /TestApp/TestApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TestApp/TestApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /TestApp/TestApp/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TestApp/TestApp/Views/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TestApp/TestApp/Views/View1A.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ©2019 SEAT, S.A. All rights reserved. 3 | // 4 | // This is file is part of a propietary app or framework. 5 | // Unauthorized reproduction, copying or modification of this file is strictly prohibited. 6 | // 7 | // This code is proprietary and confidential. 8 | // 9 | // All the 3rd-party libraries included in the project are regulated by their own licenses. 10 | // 11 | 12 | import SwiftUI 13 | import NavigationRouter 14 | 15 | /// Sample view for testing purposes 16 | struct View1A: RoutableView { 17 | // MARK: - Fields 18 | 19 | /// View model 20 | var viewModel: ViewModel1A 21 | 22 | // MARK: - Initializers 23 | 24 | /// Initializes a new instance with given view model 25 | /// - Parameter viewModel: View model instance 26 | init(viewModel: ViewModel1A) { 27 | self.viewModel = viewModel 28 | } 29 | 30 | // MARK: - View builder 31 | 32 | /// View body 33 | public var body: some View { 34 | VStack { 35 | Text("This is the root view") 36 | 37 | Spacer() 38 | 39 | VStack { 40 | RoutedLink(to: "/view1b") { 41 | Text("Navigate in-module without authentication") 42 | .accessibility(identifier: 43 | "testNavigationInSameModuleWithoutAuthentication") 44 | } 45 | RoutedLink(to: "/view1c") { 46 | Text("Navigate in-module with authentication") 47 | .accessibility(identifier: 48 | "testNavigationInSameModuleWithAuthentication") 49 | } 50 | } 51 | 52 | VStack { 53 | RoutedLink(to: "/view2a") { 54 | Text("Navigate between modules without authentication") 55 | .accessibility(identifier: 56 | "testNavigationBetweenModulesWithoutAuthentication") 57 | } 58 | RoutedLink(to: "/view2b") { 59 | Text("Navigate between modules with authentication") 60 | .accessibility(identifier: 61 | "testNavigationBetweenModulesWithAuthentication") 62 | } 63 | RoutedLink(to: "/view2c/5") { 64 | Text("Navigate between modules with parameters") 65 | .accessibility(identifier: 66 | "testNavigationBetweenModulesWithParameters") 67 | } 68 | RoutedLink(to: "/view2d") { 69 | Text("Navigate with interception (after)") 70 | .accessibility(identifier: 71 | "testInterceptionAfterNavigation") 72 | } 73 | RoutedLink(to: "/view2e") { 74 | Text("Navigate with interception (before)") 75 | .accessibility(identifier: 76 | "testInterceptionBeforeNavigation") 77 | } 78 | } 79 | 80 | VStack { 81 | RoutedLink(to: "/view3a") { 82 | Text("Navigate to View3A") 83 | .accessibility(identifier: "testView3A") 84 | } 85 | } 86 | 87 | Spacer() 88 | }.navigationBarTitle("Module 1 - Root view") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /TestApp/TestApp/Views/View2A.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ©2019 SEAT, S.A. All rights reserved. 3 | // 4 | // This is file is part of a propietary app or framework. 5 | // Unauthorized reproduction, copying or modification of this file is strictly prohibited. 6 | // 7 | // This code is proprietary and confidential. 8 | // 9 | // All the 3rd-party libraries included in the project are regulated by their own licenses. 10 | // 11 | 12 | import SwiftUI 13 | import NavigationRouter 14 | 15 | struct View2A: RoutableView { 16 | var body: some SwiftUI.View { 17 | VStack { 18 | Text("This view is from Module 2 and DOES NOT require authentication") 19 | Spacer() 20 | }.navigationBarTitle("Module 2 - No authentication") 21 | } 22 | 23 | var viewModel: ViewModel2A 24 | 25 | init(viewModel: ViewModel2A) { 26 | self.viewModel = viewModel 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /TestApp/TestApp/Views/ViewModel1A.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ©2019 SEAT, S.A. All rights reserved. 3 | // 4 | // This is file is part of a propietary app or framework. 5 | // Unauthorized reproduction, copying or modification of this file is strictly prohibited. 6 | // 7 | // This code is proprietary and confidential. 8 | // 9 | // All the 3rd-party libraries included in the project are regulated by their own licenses. 10 | // 11 | 12 | import NavigationRouter 13 | import UIKit 14 | 15 | /// Routable view model 16 | struct ViewModel1A: RoutableViewModel { 17 | // MARK: - Fields 18 | 19 | /// Required parameters 20 | static var requiredParameters: [String]? { 21 | return nil 22 | } 23 | 24 | /// Navigation interception execution flow 25 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 26 | 27 | // MARK: - Initializers 28 | 29 | init(parameters: [String : String]?) { 30 | 31 | } 32 | 33 | // MARK: - View builder 34 | 35 | /// View body 36 | var view: UIViewController { 37 | return View1A(viewModel: self) 38 | .asUIViewController() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /TestApp/TestApp/Views/ViewModel2A.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ©2019 SEAT, S.A. All rights reserved. 3 | // 4 | // This is file is part of a propietary app or framework. 5 | // Unauthorized reproduction, copying or modification of this file is strictly prohibited. 6 | // 7 | // This code is proprietary and confidential. 8 | // 9 | // All the 3rd-party libraries included in the project are regulated by their own licenses. 10 | // 11 | 12 | import NavigationRouter 13 | import UIKit 14 | 15 | /// Routable view model 16 | struct ViewModel2A: RoutableViewModel { 17 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 18 | 19 | static var requiredParameters: [String]? { 20 | return nil 21 | } 22 | 23 | init(parameters: [String : String]?) { 24 | 25 | } 26 | 27 | var view: UIViewController { 28 | return View2A(viewModel: self).asUIViewController() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /TestApp/Tests/NavigationRouterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import XCTest 24 | import Nimble 25 | 26 | @testable import NavigationRouterTestApp 27 | @testable import NavigationRouter 28 | @testable import TestFeature1 29 | @testable import TestFeature2 30 | @testable import TestFeature3 31 | 32 | /// Navigation router tests 33 | final class NavigationRouterTests: XCTestCase { 34 | // MARK: - Inner classes 35 | 36 | /// Mocked authentication handler 37 | class MockedAuthenticationHandler: RouterAuthenticationHandler { 38 | var authenticationRequested: Bool = false 39 | 40 | var isAuthenticated: Bool { 41 | return authenticationRequested 42 | } 43 | 44 | func login(completion: (() -> Void)?) { 45 | DispatchQueue.global().async { 46 | self.authenticationRequested = true 47 | 48 | DispatchQueue.global().async { 49 | completion?() 50 | } 51 | } 52 | } 53 | 54 | func logout(completion: (() -> Void)?) { 55 | // Unsupported method 56 | } 57 | 58 | func canHandleCallbackUrl(_ url: URL) -> Bool { 59 | return false 60 | } 61 | 62 | func handleCallbackUrl(_ url: URL) { 63 | // Unsupported method 64 | } 65 | } 66 | 67 | /// Mocked error handler 68 | class MockedErrorHandler: RouterErrorHandler { 69 | var errorCompletion: (() -> Void)? 70 | 71 | func handleError(_ error: RoutingError) { 72 | errorCompletion?() 73 | } 74 | } 75 | 76 | // MARK: - Fields 77 | 78 | /// Navigation router 79 | private let router: NavigationRouter = NavigationRouter.main 80 | 81 | // MARK: - Navigation binding 82 | 83 | /// Tests route binding 84 | func testRouteBinding() { 85 | // Declare test route 86 | let testNavigationRoute: NavigationRoute = NavigationRoute(path: "testPath", type: ViewModel1A.self) 87 | 88 | // Bind route 89 | NavigationRouter.bind(route: testNavigationRoute) 90 | 91 | // Expect route to be binded 92 | expect(NavigationRouter.routes).to(contain(testNavigationRoute)) 93 | } 94 | 95 | /// Tests route unbinding 96 | func testRouteUnbinding() { 97 | // Declare test route 98 | let testNavigationRoute: NavigationRoute = NavigationRoute(path: "testPath", type: ViewModel1A.self) 99 | 100 | // Bind route 101 | NavigationRouter.bind(route: testNavigationRoute) 102 | 103 | // Unbind route 104 | NavigationRouter.unbind(route: testNavigationRoute) 105 | 106 | // Expect route to be binded 107 | expect(NavigationRouter.routes).toNot(contain(testNavigationRoute)) 108 | } 109 | 110 | // MARK: - Error handling 111 | 112 | /// Tests navigation to non-registered route 113 | func testNavigationToNonRegisteredRoute() { 114 | // Create mocked handlers 115 | let mockedAuthenticationHandler: MockedAuthenticationHandler = MockedAuthenticationHandler() 116 | let mockedErrorHandler: MockedErrorHandler = MockedErrorHandler() 117 | 118 | // Prepare completion for testing 119 | let expectation: XCTestExpectation = XCTestExpectation() 120 | mockedErrorHandler.errorCompletion = { 121 | expectation.fulfill() 122 | } 123 | 124 | // Register error handler 125 | NavigationRouter.authenticationHandler = mockedAuthenticationHandler 126 | NavigationRouter.errorHandler = mockedErrorHandler 127 | 128 | // Navigate to a non-registered route 129 | self.router.navigate(toPath: "/invented/path") 130 | 131 | // Wait for expectations 132 | wait(for: [expectation], timeout: 1) 133 | } 134 | 135 | /// Tests mismatching parameters 136 | func testMismatchingParameters() { 137 | // Ensure routing with mismatching parameters causes an error 138 | let mockedErrorHandler: MockedErrorHandler = MockedErrorHandler() 139 | 140 | // Create expectation 141 | let expectation: XCTestExpectation = XCTestExpectation() 142 | 143 | // Prepare completion for testing 144 | mockedErrorHandler.errorCompletion = { 145 | expectation.fulfill() 146 | } 147 | 148 | // Register error handler 149 | NavigationRouter.errorHandler = mockedErrorHandler 150 | 151 | // Navigate with wrong parameters 152 | self.router.navigate(toPath: "/view2C") 153 | 154 | // Ensure error handled 155 | wait(for: [expectation], timeout: 1) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /TestApp/UITests/NavigationRouterUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import XCTest 24 | import Nimble 25 | 26 | @testable import NavigationRouterTestApp 27 | @testable import NavigationRouter 28 | @testable import TestFeature1 29 | @testable import TestFeature2 30 | @testable import TestFeature3 31 | 32 | /// Navigation router UI tests 33 | final class NavigationRouterUITests: XCTestCase { 34 | // MARK: - Fields 35 | 36 | /// Navigation router 37 | private let router: NavigationRouter = NavigationRouter.main 38 | 39 | // MARK: - Set up 40 | 41 | /// Setups test case 42 | override func setUp() { 43 | super.setUp() 44 | 45 | // Do not continue after failure 46 | self.continueAfterFailure = false 47 | 48 | // Create app instance 49 | let app: XCUIApplication = XCUIApplication() 50 | 51 | // Launch app before each test execution 52 | app.launch() 53 | } 54 | 55 | // MARK: - Navigation tests 56 | 57 | /// Tests navigation in same module without authentication 58 | func testNavigationInSameModuleWithoutAuthentication() { 59 | // Get app instance 60 | let app: XCUIApplication = XCUIApplication() 61 | 62 | // Tap first option 63 | app.staticTexts["testNavigationInSameModuleWithoutAuthentication"].tap() 64 | 65 | // Expect corresponding view to appear 66 | _ = app.staticTexts["This view is from Module 1 and DOES NOT require authentication"].waitForExistence(timeout: 1) 67 | } 68 | 69 | /// Tests navigation in same module with authentication 70 | func testNavigationInSameModuleWithAuthentication() { 71 | // Get app instance 72 | let app: XCUIApplication = XCUIApplication() 73 | 74 | // Tap first option 75 | app.staticTexts["testNavigationInSameModuleWithAuthentication"].tap() 76 | 77 | // Expect alert to appear 78 | let okButton: XCUIElement = app.buttons["OK"] 79 | _ = okButton.waitForExistence(timeout: 2) 80 | 81 | // Tap to dismiss 82 | okButton.tap() 83 | 84 | // Expect corresponding view to appear 85 | _ = app.staticTexts["This view is from Module 1 and requires authentication"].waitForExistence(timeout: 1) 86 | } 87 | 88 | /// Tests navigation between modules without authentication 89 | func testNavigationBetweenModulesWithoutAuthentication() { 90 | // Get app instance 91 | let app: XCUIApplication = XCUIApplication() 92 | 93 | // Tap first option 94 | app.staticTexts["testNavigationBetweenModulesWithoutAuthentication"].tap() 95 | 96 | // Expect corresponding view to appear 97 | _ = app.staticTexts["This view is from Module 2 and DOES NOT require authentication"].waitForExistence(timeout: 1) 98 | } 99 | 100 | /// Tests navigation between modules with authentication 101 | func testNavigationBetweenModulesWithAuthentication() { 102 | // Get app instance 103 | let app: XCUIApplication = XCUIApplication() 104 | 105 | // Tap first option 106 | app.staticTexts["testNavigationBetweenModulesWithAuthentication"].tap() 107 | 108 | // Expect alert to appear 109 | let okButton: XCUIElement = app.buttons["OK"] 110 | _ = okButton.waitForExistence(timeout: 2) 111 | 112 | // Tap to dismiss 113 | okButton.tap() 114 | 115 | // Expect corresponding view to appear 116 | _ = app.staticTexts["This view is from Module 2 and requires authentication"].waitForExistence(timeout: 1) 117 | } 118 | 119 | /// Tests external navigation 120 | func testExternalNavigation() { 121 | // Declare external navigation link 122 | let externalNavigationLink: String = "routertestapp:/navigate/view2c/10" // Check SceneDelegate from TestApp for implementation 123 | 124 | // Send app to background by pressing home button 125 | XCUIDevice.shared.press(.home) 126 | 127 | // Open Safari 128 | let safariApp = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari") 129 | safariApp.launch() 130 | 131 | // Type external navigation link in location bar and navigate 132 | safariApp.typeText(externalNavigationLink + "\n") 133 | 134 | // Tap ok button from system alert, it is always the latest one in hierarchy 135 | safariApp.buttons["Open"].tap() 136 | 137 | // Get app instance 138 | let app: XCUIApplication = XCUIApplication() 139 | 140 | // Ensure expected view appears 141 | _ = app.staticTexts["Parameter id: 10"].waitForExistence(timeout: 5) 142 | } 143 | 144 | /// Tests interception before navigation 145 | func testInterceptionBeforeNavigation() { 146 | // Get app instance 147 | let app: XCUIApplication = XCUIApplication() 148 | 149 | // Tap first option 150 | app.staticTexts["testInterceptionBeforeNavigation"].tap() 151 | 152 | // Expect corresponding view to appear 153 | _ = app.staticTexts["This is the fifth view of Module 3"].waitForExistence(timeout: 1) 154 | 155 | // Tap 156 | app.staticTexts["testContinueInterceptor"].tap() 157 | 158 | // Expect corresponding view to appear 159 | _ = app.staticTexts["This view is intercepted before navigating"].waitForExistence(timeout: 1) 160 | } 161 | 162 | /// Tests interception after navigation 163 | func testInterceptionAfterNavigation() { 164 | // Get app instance 165 | let app: XCUIApplication = XCUIApplication() 166 | 167 | // Tap first option 168 | app.staticTexts["testInterceptionAfterNavigation"].tap() 169 | 170 | // Expect corresponding view to appear 171 | _ = app.staticTexts["This is the fourth view of Module 3"].waitForExistence(timeout: 1) 172 | 173 | // Tap 174 | app.buttons["testDismissInterceptor"].tap() 175 | 176 | // Expect corresponding view to appear 177 | _ = app.staticTexts["This view is intercepted after navigation"].waitForExistence(timeout: 1) 178 | } 179 | 180 | /// Tests navigation stack handling 181 | func testNavigationStackHandling() { 182 | // Get app instance 183 | let app: XCUIApplication = XCUIApplication() 184 | 185 | // Tap first option 186 | app.staticTexts["testMultipleNavigations1"].tap() 187 | 188 | // Expect corresponding view to appear 189 | _ = app.staticTexts["This is the first view of Module 3."].waitForExistence(timeout: 1) 190 | 191 | // Tap first option 192 | app.staticTexts["testMultipleNavigations2"].tap() 193 | 194 | // Expect corresponding view to appear 195 | _ = app.staticTexts["This is the second view of Module 3."].waitForExistence(timeout: 1) 196 | 197 | // Tap first option 198 | app.staticTexts["testMultipleNavigations3"].tap() 199 | 200 | // Expect corresponding view to appear 201 | _ = app.staticTexts["This is the third view of Module 3."].waitForExistence(timeout: 1) 202 | 203 | // Tap first option 204 | app.staticTexts["testMultipleNavigations4"].tap() 205 | 206 | // Expect corresponding view to appear 207 | _ = app.staticTexts["View 1A"].waitForExistence(timeout: 1) 208 | _ = app.staticTexts["Navigation stack handling"].waitForExistence(timeout: 1) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /TestFeature1/TestFeature1/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /TestFeature1/TestFeature1/TestFeature1Module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | 25 | /// Test feature 1 module definition 26 | public final class TestFeature1Module: RoutableModule { 27 | // MARK: - Initializers 28 | 29 | /// Initializes a new instance 30 | public init() { 31 | // Initialize instance here as needed 32 | } 33 | 34 | // MARK: - Routing 35 | 36 | /// Registers navigation routers 37 | public func registerRoutes() { 38 | // Define routes 39 | let view1ARoute: NavigationRoute = NavigationRoute( 40 | path: "/view1A", 41 | type: ViewModel1A.self, 42 | requiresAuthentication: false) 43 | let view1BRoute: NavigationRoute = NavigationRoute( 44 | path: "/view1B", 45 | type: ViewModel1B.self, 46 | requiresAuthentication: false, 47 | allowedExternally: true) 48 | let view1CRoute: NavigationRoute = NavigationRoute( 49 | path: "/view1C", 50 | type: ViewModel1C.self, 51 | requiresAuthentication: true) 52 | 53 | // Register routes 54 | NavigationRouter.bind(route: view1ARoute) 55 | NavigationRouter.bind(route: view1BRoute) 56 | NavigationRouter.bind(route: view1CRoute) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /TestFeature1/TestFeature1/ViewModels/ViewModel1A.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | /// Routable view model 27 | struct ViewModel1A: RoutableViewModel { 28 | // MARK: - Routing 29 | 30 | /// Required navigation parameters (if any) 31 | static var requiredParameters: [String]? 32 | 33 | /// Navigation interception execution flow (if any) 34 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 35 | 36 | /// Initializes a new instance 37 | /// - Parameter parameters: Navigation parameters 38 | init(parameters: [String : String]?) { 39 | // Do something with parameters (e.g. instantiating a model) 40 | } 41 | 42 | /// View body 43 | var routedView: AnyView { 44 | View1A(viewModel: self) 45 | .eraseToAnyView() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /TestFeature1/TestFeature1/ViewModels/ViewModel1B.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | struct ViewModel1B: RoutableViewModel { 28 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | static var requiredParameters: [String]? { 31 | return nil 32 | } 33 | 34 | init(parameters: [String : String]?) { 35 | 36 | } 37 | 38 | var routedView: AnyView { 39 | View1B(viewModel: self) 40 | .eraseToAnyView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TestFeature1/TestFeature1/ViewModels/ViewModel1C.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | struct ViewModel1C: RoutableViewModel { 28 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | static var requiredParameters: [String]? { 31 | return nil 32 | } 33 | 34 | init(parameters: [String : String]?) { 35 | 36 | } 37 | 38 | var routedView: AnyView { 39 | View1C(viewModel: self) 40 | .eraseToAnyView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TestFeature1/TestFeature1/Views/View1A.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | /// View 1A 27 | struct View1A: RoutableView { 28 | // MARK: - View body 29 | 30 | /// Body builder 31 | var body: some View { 32 | ScrollView { 33 | VStack(alignment: .leading, spacing: 20) { 34 | Text("This is the root view, set as root view controller for active scene using UIHostingController.") 35 | .font(.body) 36 | .foregroundColor(.secondary) 37 | 38 | VStack(alignment: .leading, spacing: 10) { 39 | Text("In-module navigation") 40 | .font(.headline) 41 | 42 | RoutedLink(to: "/view1B") { 43 | HStack { 44 | VStack(alignment: .leading, spacing: 10) { 45 | Text("Navigate to View 1B") 46 | .bold() 47 | .foregroundColor(Color(UIColor.systemBackground)) 48 | .accessibility(identifier: "testNavigationInSameModuleWithoutAuthentication") 49 | 50 | Text("(without authentication)") 51 | .font(.footnote) 52 | .foregroundColor(Color(UIColor.systemBackground)) 53 | } 54 | 55 | Spacer() 56 | 57 | Image(systemName: "chevron.right") 58 | .foregroundColor(Color(UIColor.systemBackground)) 59 | } 60 | .padding() 61 | } 62 | .background(Color.primary) 63 | .cornerRadius(4) 64 | 65 | RoutedLink(to: "/view1C") { 66 | HStack { 67 | VStack(alignment: .leading, spacing: 10) { 68 | HStack { 69 | Text("Navigate to View 1C") 70 | .bold() 71 | .foregroundColor(Color(UIColor.systemBackground)) 72 | .accessibility(identifier: "testNavigationInSameModuleWithAuthentication") 73 | 74 | Spacer() 75 | } 76 | 77 | Text("(with authentication)") 78 | .font(.footnote) 79 | .foregroundColor(Color(UIColor.systemBackground)) 80 | } 81 | 82 | Spacer() 83 | 84 | Image(systemName: "chevron.right") 85 | .foregroundColor(Color(UIColor.systemBackground)) 86 | } 87 | .padding() 88 | } 89 | .background(Color.primary) 90 | .cornerRadius(4) 91 | } 92 | 93 | Divider() 94 | 95 | VStack(alignment: .leading, spacing: 10) { 96 | Text("Cross-module navigation") 97 | .font(.headline) 98 | 99 | RoutedLink(to: "/view2A") { 100 | HStack { 101 | VStack(alignment: .leading, spacing: 10) { 102 | Text("Navigate to View 2A") 103 | .bold() 104 | .foregroundColor(.white) 105 | .accessibility(identifier: "testNavigationBetweenModulesWithoutAuthentication") 106 | 107 | Text("(without authentication)") 108 | .font(.footnote) 109 | .foregroundColor(.white) 110 | } 111 | 112 | Spacer() 113 | 114 | Image(systemName: "chevron.right") 115 | .foregroundColor(Color(UIColor.systemBackground)) 116 | } 117 | .padding() 118 | } 119 | .background(Color.blue) 120 | .cornerRadius(4) 121 | 122 | RoutedLink(to: "/view2B") { 123 | HStack { 124 | VStack(alignment: .leading, spacing: 10) { 125 | Text("Navigate to View 2B") 126 | .bold() 127 | .foregroundColor(.white) 128 | .accessibility(identifier: "testNavigationBetweenModulesWithAuthentication") 129 | 130 | Text("(with authentication)") 131 | .font(.footnote) 132 | .foregroundColor(.white) 133 | } 134 | 135 | Spacer() 136 | 137 | Image(systemName: "chevron.right") 138 | .foregroundColor(Color(UIColor.systemBackground)) 139 | } 140 | .padding() 141 | } 142 | .background(Color.blue) 143 | .cornerRadius(4) 144 | } 145 | 146 | Divider() 147 | 148 | VStack(alignment: .leading, spacing: 10) { 149 | Text("Parametrized navigation") 150 | .font(.headline) 151 | 152 | RoutedLink(to: "/view2C/5") { 153 | HStack { 154 | VStack(alignment: .leading, spacing: 10) { 155 | Text("Navigate to View 2C") 156 | .bold() 157 | .foregroundColor(.white) 158 | .accessibility(identifier: "testNavigationBetweenModulesWithParameters") 159 | 160 | Text("(with parameters)") 161 | .font(.footnote) 162 | .foregroundColor(.white) 163 | } 164 | 165 | Spacer() 166 | 167 | Image(systemName: "chevron.right") 168 | .foregroundColor(Color(UIColor.systemBackground)) 169 | } 170 | .padding() 171 | } 172 | .background(Color.red) 173 | .cornerRadius(4) 174 | } 175 | 176 | Divider() 177 | 178 | VStack(alignment: .leading, spacing: 10) { 179 | Text("Intercepting navigation") 180 | .font(.headline) 181 | 182 | RoutedLink(to: "/view2D") { 183 | HStack { 184 | VStack(alignment: .leading, spacing: 10) { 185 | Text("Navigate to View 2D") 186 | .bold() 187 | .foregroundColor(.white) 188 | .accessibility(identifier: "testInterceptionAfterNavigation") 189 | 190 | Text("(after navigation)") 191 | .font(.footnote) 192 | .foregroundColor(.white) 193 | } 194 | 195 | Spacer() 196 | 197 | Image(systemName: "chevron.right") 198 | .foregroundColor(Color(UIColor.systemBackground)) 199 | } 200 | .padding() 201 | } 202 | .background(Color.purple) 203 | .cornerRadius(4) 204 | 205 | RoutedLink(to: "/view2E") { 206 | HStack { 207 | VStack(alignment: .leading, spacing: 10) { 208 | Text("Navigate to View 2E") 209 | .bold() 210 | .foregroundColor(.white) 211 | .accessibility(identifier: "testInterceptionBeforeNavigation") 212 | 213 | Text("(before navigation)") 214 | .font(.footnote) 215 | .foregroundColor(.white) 216 | } 217 | 218 | Spacer() 219 | 220 | Image(systemName: "chevron.right") 221 | .foregroundColor(Color(UIColor.systemBackground)) 222 | } 223 | .padding() 224 | } 225 | .background(Color.purple) 226 | .cornerRadius(4) 227 | } 228 | 229 | Divider() 230 | 231 | VStack(alignment: .leading, spacing: 10) { 232 | Text("Navigation stack handling") 233 | .font(.headline) 234 | 235 | RoutedLink(to: "/view3A") { 236 | HStack { 237 | VStack(alignment: .leading, spacing: 10) { 238 | Text("Navigate to View 3A") 239 | .bold() 240 | .foregroundColor(.white) 241 | .accessibility(identifier: "testMultipleNavigations1") 242 | 243 | Text("(multiple navigations)") 244 | .font(.footnote) 245 | .foregroundColor(.white) 246 | } 247 | 248 | Spacer() 249 | 250 | Image(systemName: "chevron.right") 251 | .foregroundColor(Color(UIColor.systemBackground)) 252 | } 253 | .padding() 254 | } 255 | .background(Color(UIColor.brown)) 256 | .cornerRadius(4) 257 | } 258 | } 259 | .padding() 260 | }.navigationBarTitle("View 1A", displayMode: .large) 261 | } 262 | 263 | // MARK: - Fields 264 | 265 | /// View model instance 266 | var viewModel: ViewModel1A 267 | 268 | // MARK: - Initializers 269 | 270 | /// Initializes a new instance with given view model 271 | /// - Parameter viewModel: View model instance 272 | init(viewModel: ViewModel1A) { 273 | self.viewModel = viewModel 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /TestFeature1/TestFeature1/Views/View1B.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | struct View1B: RoutableView { 27 | public var viewModel: ViewModel1B 28 | 29 | public init(viewModel: ViewModel1B) { 30 | self.viewModel = viewModel 31 | } 32 | 33 | var body: some View { 34 | ScrollView { 35 | VStack(alignment: .leading, spacing: 20) { 36 | Text("This view is from Module 1 and DOES NOT require authentication.") 37 | .font(.body) 38 | .foregroundColor(.secondary) 39 | } 40 | .padding() 41 | } 42 | .navigationBarTitle("View 1B", displayMode: .large) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TestFeature1/TestFeature1/Views/View1C.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | struct View1C: View { 27 | var body: some View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This view is from Module 1 and requires authentication.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | } 34 | .padding() 35 | } 36 | .navigationBarTitle("View 1C", displayMode: .large) 37 | } 38 | 39 | var viewModel: ViewModel1C 40 | 41 | init(viewModel: ViewModel1C) { 42 | self.viewModel = viewModel 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/TestFeature2Module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | 25 | /// Test feature 2 module definition 26 | public final class TestFeature2Module: RoutableModule { 27 | // MARK: - Initializers 28 | 29 | /// Initializes a new instance 30 | public init() { 31 | // Initialize instance here as needed 32 | } 33 | 34 | // MARK: - Routing 35 | 36 | /// Registers navigation routers 37 | public func registerRoutes() { 38 | // Define routes 39 | let view2ARoute: NavigationRoute = NavigationRoute(path: "/view2A", 40 | type: ViewModel2A.self, 41 | requiresAuthentication: false) 42 | let view2BRoute: NavigationRoute = NavigationRoute(path: "/view2B", 43 | type: ViewModel2B.self, 44 | requiresAuthentication: true) 45 | let view2CRoute: NavigationRoute = NavigationRoute(path: "/view2C/:id", 46 | type: ViewModel2C.self, 47 | requiresAuthentication: false, 48 | allowedExternally: true) 49 | let view2DRoute: NavigationRoute = NavigationRoute(path: "/view2D", 50 | type: ViewModel2D.self, 51 | requiresAuthentication: false, 52 | allowedExternally: true) 53 | let view2ERoute: NavigationRoute = NavigationRoute(path: "/view2E", 54 | type: ViewModel2E.self, 55 | requiresAuthentication: false, 56 | allowedExternally: true) 57 | 58 | // Register routes 59 | NavigationRouter.bind(routes: [view2ARoute, 60 | view2BRoute, 61 | view2CRoute, 62 | view2DRoute, 63 | view2ERoute]) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/ViewModels/ViewModel2A.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | /// Routable view model 27 | struct ViewModel2A: RoutableViewModel { 28 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | static var requiredParameters: [String]? 31 | 32 | init(parameters: [String : String]?) { 33 | 34 | } 35 | 36 | var routedView: AnyView { 37 | View2A(viewModel: self) 38 | .eraseToAnyView() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/ViewModels/ViewModel2B.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | struct ViewModel2B: RoutableViewModel { 28 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | static var requiredParameters: [String]? 31 | 32 | init(parameters: [String : String]?) { 33 | 34 | } 35 | 36 | var routedView: AnyView { 37 | View2B(viewModel: self) 38 | .eraseToAnyView() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/ViewModels/ViewModel2C.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | struct ViewModel2C: RoutableViewModel { 28 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | // MARK: - Static fields 31 | 32 | /// Required parameters 33 | static var requiredParameters: [String]? { 34 | return [ 35 | "id" 36 | ] 37 | } 38 | 39 | // MARK: - Fields 40 | 41 | /// Identifier (parameter) 42 | var id: String? 43 | 44 | // MARK: - Initializers 45 | 46 | /// Initializes a new instance with given parameters 47 | /// - Parameter parameters: Parameters used for navigation 48 | init(parameters: [String : String]?) { 49 | self.id = parameters?["id"] 50 | } 51 | 52 | // MARK: - View builder 53 | 54 | /// Makes view for navigation 55 | var routedView: AnyView { 56 | View2C(viewModel: self) 57 | .eraseToAnyView() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/ViewModels/ViewModel2D.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | struct ViewModel2D: RoutableViewModel { 28 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | static var requiredParameters: [String]? { 31 | return nil 32 | } 33 | 34 | init(parameters: [String : String]?) { 35 | 36 | } 37 | 38 | var routedView: AnyView { 39 | View2D(viewModel: self) 40 | .eraseToAnyView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/ViewModels/ViewModel2E.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | struct ViewModel2E: RoutableViewModel { 28 | var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | static var requiredParameters: [String]? { 31 | return nil 32 | } 33 | 34 | init(parameters: [String : String]?) { 35 | 36 | } 37 | 38 | var routedView: AnyView { 39 | View2E(viewModel: self) 40 | .eraseToAnyView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/Views/View2A.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | struct View2A: RoutableView { 27 | var body: some View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This view is from Module 2 and DOES NOT require authentication.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | } 34 | .padding() 35 | } 36 | .navigationBarTitle("View 2A", displayMode: .large) 37 | } 38 | 39 | var viewModel: ViewModel2A 40 | 41 | init(viewModel: ViewModel2A) { 42 | self.viewModel = viewModel 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/Views/View2B.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | struct View2B: RoutableView { 27 | var body: some View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This view is from Module 2 and requires authentication.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | } 34 | .padding() 35 | } 36 | .navigationBarTitle("View 2B", displayMode: .large) 37 | } 38 | 39 | var viewModel: ViewModel2B 40 | 41 | init(viewModel: ViewModel2B) { 42 | self.viewModel = viewModel 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/Views/View2C.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | struct View2C: RoutableView { 27 | var body: some View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This view is from Module 2 and receives parameters.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | Text("Parameter id: \(viewModel.id ?? "")") 34 | .font(.body) 35 | .foregroundColor(.secondary) 36 | } 37 | .padding() 38 | } 39 | .navigationBarTitle("View 2C", displayMode: .large) 40 | } 41 | 42 | var viewModel: ViewModel2C 43 | 44 | init(viewModel: ViewModel2C) { 45 | self.viewModel = viewModel 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/Views/View2D.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | struct View2D: RoutableView { 27 | var body: some View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This view is intercepted after navigation.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | } 34 | .padding() 35 | } 36 | .navigationBarTitle("View 2D", displayMode: .large) 37 | } 38 | 39 | var viewModel: ViewModel2D 40 | 41 | init(viewModel: ViewModel2D) { 42 | self.viewModel = viewModel 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TestFeature2/TestFeature2/Views/View2E.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | struct View2E: RoutableView { 27 | var body: some View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This view is intercepted before navigating.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | } 34 | .padding() 35 | } 36 | .navigationBarTitle("View 2E", displayMode: .large) 37 | } 38 | 39 | var viewModel: ViewModel2E 40 | 41 | init(viewModel: ViewModel2E) { 42 | self.viewModel = viewModel 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/TestFeature3Module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | 25 | /// Test feature 3 module definition 26 | public final class TestFeature3Module: RoutableModule { 27 | // MARK: - Initializers 28 | 29 | /// Initializes a new instance 30 | public init() { 31 | // Initialize instance here as needed 32 | } 33 | 34 | // MARK: - Routing 35 | 36 | /// Registers navigation routers 37 | public func registerRoutes() { 38 | // Define routes 39 | let view3ARoute: NavigationRoute = NavigationRoute(path: "/view3A", 40 | type: ViewModel3A.self, 41 | requiresAuthentication: false) 42 | let view3BRoute: NavigationRoute = NavigationRoute(path: "/view3B", 43 | type: ViewModel3B.self, 44 | requiresAuthentication: false) 45 | let view3CRoute: NavigationRoute = NavigationRoute(path: "/view3C", 46 | type: ViewModel3C.self, 47 | requiresAuthentication: false) 48 | let view3DRoute: NavigationRoute = NavigationRoute(path: "/view3D", 49 | type: ViewModel3D.self, 50 | requiresAuthentication: false) 51 | let view3ERoute: NavigationRoute = NavigationRoute(path: "/view3E", 52 | type: ViewModel3E.self, 53 | requiresAuthentication: false) 54 | 55 | // Register routes 56 | NavigationRouter.bind(routes: [ 57 | view3ARoute, 58 | view3BRoute, 59 | view3CRoute, 60 | view3DRoute, 61 | view3ERoute 62 | ]) 63 | } 64 | 65 | /// Registers interceptors 66 | public func registerInterceptors() { 67 | // Intercept view 2D 68 | NavigationRouter.interceptNavigation( 69 | toPath: "/view2D", 70 | when: .after, 71 | withPriority: .low, 72 | isAuthenticationRequired: false) { router, executionFlow in 73 | let interceptionFlow: NavigationInterceptionFlow = NavigationInterceptionFlow(completion: executionFlow) 74 | router.navigate(toPath: "/view3D", 75 | modal: true, 76 | interceptionExecutionFlow: interceptionFlow) 77 | } 78 | 79 | // Intercept view 2E 80 | NavigationRouter.interceptNavigation( 81 | toPath: "/view2E", 82 | when: .before, 83 | withPriority: .low, 84 | isAuthenticationRequired: false) { router, executionFlow in 85 | let interceptionFlow: NavigationInterceptionFlow = NavigationInterceptionFlow(completion: executionFlow) 86 | router.navigate(toPath: "/view3E", 87 | interceptionExecutionFlow: interceptionFlow) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/ViewModels/ViewModel3A.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | public class ViewModel3A: RoutableViewModel { 28 | public var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | public static var requiredParameters: [String]? { 31 | return nil 32 | } 33 | 34 | public required init(parameters: [String: String]?) { 35 | 36 | } 37 | 38 | public var routedView: AnyView { 39 | View3A(viewModel: self) 40 | .eraseToAnyView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/ViewModels/ViewModel3B.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | public class ViewModel3B: RoutableViewModel { 28 | public var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | public static var requiredParameters: [String]? { 31 | return nil 32 | } 33 | 34 | public required init(parameters: [String: String]?) { 35 | 36 | } 37 | 38 | public var routedView: AnyView { 39 | View3B(viewModel: self) 40 | .eraseToAnyView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/ViewModels/ViewModel3C.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | public class ViewModel3C: RoutableViewModel { 28 | public var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | public static var requiredParameters: [String]? { 31 | return nil 32 | } 33 | 34 | public required init(parameters: [String: String]?) { 35 | 36 | } 37 | 38 | public var routedView: AnyView { 39 | View3C(viewModel: self) 40 | .eraseToAnyView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/ViewModels/ViewModel3D.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | public class ViewModel3D: RoutableViewModel { 28 | public var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | public static var requiredParameters: [String]? { 31 | return nil 32 | } 33 | 34 | public required init(parameters: [String: String]?) { 35 | 36 | } 37 | 38 | public var routedView: AnyView { 39 | View3D(viewModel: self) 40 | .eraseToAnyView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/ViewModels/ViewModel3E.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import NavigationRouter 24 | import SwiftUI 25 | 26 | /// Routable view model 27 | public class ViewModel3E: RoutableViewModel { 28 | public var navigationInterceptionExecutionFlow: NavigationInterceptionFlow? 29 | 30 | public static var requiredParameters: [String]? { 31 | return nil 32 | } 33 | 34 | public required init(parameters: [String: String]?) { 35 | 36 | } 37 | 38 | public var routedView: AnyView { 39 | View3E(viewModel: self) 40 | .eraseToAnyView() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/Views/View3A.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | public struct View3A: RoutableView { 27 | public var body: some SwiftUI.View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This is the first view of Module 3.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | 34 | Spacer() 35 | 36 | RoutedLink(to: "/view3B") { 37 | HStack { 38 | VStack(alignment: .leading, spacing: 10) { 39 | Text("Navigate to view 3B") 40 | .bold() 41 | .foregroundColor(.white) 42 | .accessibility(identifier: "testMultipleNavigations2") 43 | 44 | Text("(without replacing stack)") 45 | .font(.footnote) 46 | .foregroundColor(.white) 47 | } 48 | 49 | Spacer() 50 | 51 | Image(systemName: "chevron.right") 52 | .foregroundColor(Color(UIColor.systemBackground)) 53 | } 54 | .padding() 55 | } 56 | .background(Color(UIColor.brown)) 57 | .cornerRadius(4) 58 | } 59 | .padding() 60 | }.navigationBarTitle("View 3A", displayMode: .large) 61 | } 62 | 63 | public var viewModel: ViewModel3A 64 | 65 | public init(viewModel: ViewModel3A) { 66 | self.viewModel = viewModel 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/Views/View3B.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | public struct View3B: RoutableView { 27 | public var body: some SwiftUI.View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This is the second view of Module 3.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | 34 | Spacer() 35 | 36 | RoutedLink(to: "/view3C", replace: true, animation: .left) { 37 | HStack { 38 | VStack(alignment: .leading, spacing: 10) { 39 | Text("Navigate to view 3C") 40 | .bold() 41 | .foregroundColor(.white) 42 | .accessibility(identifier: "testMultipleNavigations3") 43 | 44 | Text("(replacing stack)") 45 | .font(.footnote) 46 | .foregroundColor(.white) 47 | } 48 | 49 | Spacer() 50 | 51 | Image(systemName: "chevron.right") 52 | .foregroundColor(Color(UIColor.systemBackground)) 53 | } 54 | .padding() 55 | } 56 | .background(Color(UIColor.brown)) 57 | .cornerRadius(4) 58 | } 59 | .padding() 60 | } 61 | .navigationBarTitle("View 3B", displayMode: .large) 62 | } 63 | 64 | public var viewModel: ViewModel3B 65 | 66 | public init(viewModel: ViewModel3B) { 67 | self.viewModel = viewModel 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/Views/View3C.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | public struct View3C: RoutableView { 27 | public var body: some SwiftUI.View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This is the third view of Module 3.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | 34 | Spacer() 35 | 36 | RoutedLink(to: "/view1A", replace: true, animation: .right) { 37 | HStack { 38 | VStack(alignment: .leading, spacing: 10) { 39 | Text("Navigate to view 1A") 40 | .bold() 41 | .foregroundColor(.white) 42 | .accessibility(identifier: "testMultipleNavigations4") 43 | 44 | Text("(back to root)") 45 | .font(.footnote) 46 | .foregroundColor(.white) 47 | } 48 | 49 | Spacer() 50 | 51 | Image(systemName: "chevron.right") 52 | .foregroundColor(Color(UIColor.systemBackground)) 53 | } 54 | .padding() 55 | } 56 | .background(Color(UIColor.brown)) 57 | .cornerRadius(4) 58 | } 59 | .padding() 60 | } 61 | .navigationBarTitle("View 3C", displayMode: .large) 62 | } 63 | 64 | public var viewModel: ViewModel3C 65 | 66 | public init(viewModel: ViewModel3C) { 67 | self.viewModel = viewModel 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/Views/View3D.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | public struct View3D: RoutableView { 27 | public var body: some SwiftUI.View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This is the fourth view of Module 3.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | 34 | Text("It is intended to be an interceptor for View 2D.") 35 | .font(.body) 36 | .foregroundColor(.secondary) 37 | 38 | Spacer() 39 | } 40 | .padding() 41 | } 42 | .navigationBarTitle("View 3D", displayMode: .inline) 43 | .navigationBarItems(leading: self.navigationBarItemsLeading) 44 | } 45 | 46 | /// Navigation bar items (leading) 47 | private var navigationBarItemsLeading: some View { 48 | HStack { 49 | Button(action: { 50 | NavigationRouter.main.dismissModalIfNeeded() 51 | }, label: { 52 | Image(systemName: "xmark") 53 | }) 54 | .accessibility(identifier: "testDismissInterceptor") 55 | } 56 | } 57 | 58 | public var viewModel: ViewModel3D 59 | 60 | public init(viewModel: ViewModel3D) { 61 | self.viewModel = viewModel 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /TestFeature3/TestFeature3/Views/View3E.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Cristian Ortega Gómez (https://github.com/corteggo) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import SwiftUI 24 | import NavigationRouter 25 | 26 | public struct View3E: RoutableView { 27 | public var body: some SwiftUI.View { 28 | ScrollView { 29 | VStack(alignment: .leading, spacing: 20) { 30 | Text("This is the fifth view of Module 3.") 31 | .font(.body) 32 | .foregroundColor(.secondary) 33 | 34 | Text("It is used as an interceptor for View 2E.") 35 | .font(.body) 36 | .foregroundColor(.secondary) 37 | 38 | Spacer() 39 | 40 | HStack { 41 | VStack(alignment: .leading, spacing: 10) { 42 | Text("Continue") 43 | .bold() 44 | .foregroundColor(.white) 45 | .accessibility(identifier: "testContinueInterceptor") 46 | 47 | Text("(to View 2E)") 48 | .font(.footnote) 49 | .foregroundColor(.white) 50 | } 51 | 52 | Spacer() 53 | 54 | Image(systemName: "chevron.right") 55 | .foregroundColor(Color(UIColor.systemBackground)) 56 | } 57 | .padding() 58 | .background(Color(UIColor.brown)) 59 | .cornerRadius(4) 60 | .onTapGesture { 61 | // Continue original navigation flow 62 | self.viewModel 63 | .navigationInterceptionExecutionFlow? 64 | .continue?(false) 65 | } 66 | } 67 | .padding() 68 | } 69 | .navigationBarTitle("View 3E", displayMode: .large) 70 | } 71 | 72 | public var viewModel: ViewModel3E 73 | 74 | public init(viewModel: ViewModel3E) { 75 | self.viewModel = viewModel 76 | } 77 | } 78 | --------------------------------------------------------------------------------