├── TestApp
├── TestApp
│ ├── Resources
│ │ └── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Views
│ │ ├── View2A.swift
│ │ ├── ViewModel2A.swift
│ │ ├── ViewModel1A.swift
│ │ ├── Base.lproj
│ │ │ └── LaunchScreen.storyboard
│ │ └── View1A.swift
│ └── Delegates
│ │ ├── AppDelegate.swift
│ │ └── SceneDelegate.swift
├── NavigationRouterTestApp.entitlements
├── Supporting files
│ ├── TestAppTests-Info.plist
│ ├── TestAppUITests-Info.plist
│ └── TestApp-Info.plist
├── TestApp.xcodeproj
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── NavigationRouterTestApp.xcscheme
├── Tests
│ └── NavigationRouterTests.swift
└── UITests
│ └── NavigationRouterUITests.swift
├── .codecov.yml
├── NavigationRouter.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ └── NavigationRouter.xcscheme
├── README.md
├── NavigationRouter.xcworkspace
├── xcshareddata
│ └── IDEWorkspaceChecks.plist
└── contents.xcworkspacedata
├── .github
└── workflows
│ ├── build.yml
│ └── test.yml
├── Package.swift
├── Code
├── Extensions
│ ├── SwiftUIView+TypeErasure.swift
│ ├── DispatchQueue.swift
│ ├── UIWindow+KeyWindow.swift
│ └── UIWindow+Animation.swift
├── Protocols
│ ├── RouterErrorHandler.swift
│ ├── Route.swift
│ ├── RouterAuthenticationHandler.swift
│ ├── MVVM
│ │ └── Router+MVVM.swift
│ ├── Routable.swift
│ └── Router.swift
├── Errors
│ └── RoutingError.swift
├── Routing
│ ├── Extensions
│ │ ├── NavigationRouter+ErrorHandling.swift
│ │ ├── NavigationRouter+PathMatcher.swift
│ │ ├── NavigationRouter+NavigationHandling.swift
│ │ └── NavigationRouter+NavigationInterception.swift
│ └── NavigationRouter.swift
├── Helpers
│ ├── TransitionAnimation.swift
│ └── PathMatcher.swift
├── Modules
│ ├── RoutableModule.swift
│ └── RoutableModulesFactory.swift
├── Models
│ └── NavigationRoute.swift
└── Views
│ └── RoutedLink.swift
├── .swiftlint.yml
├── CHANGELOG.md
├── TestFeature1
└── TestFeature1
│ ├── Info.plist
│ ├── ViewModels
│ ├── ViewModel1B.swift
│ ├── ViewModel1C.swift
│ └── ViewModel1A.swift
│ ├── Views
│ ├── View1C.swift
│ ├── View1B.swift
│ └── View1A.swift
│ └── TestFeature1Module.swift
├── TestFeature2
└── TestFeature2
│ ├── Info.plist
│ ├── ViewModels
│ ├── ViewModel2A.swift
│ ├── ViewModel2B.swift
│ ├── ViewModel2D.swift
│ ├── ViewModel2E.swift
│ └── ViewModel2C.swift
│ ├── Views
│ ├── View2D.swift
│ ├── View2E.swift
│ ├── View2B.swift
│ ├── View2A.swift
│ └── View2C.swift
│ └── TestFeature2Module.swift
├── TestFeature3
└── TestFeature3
│ ├── Info.plist
│ ├── ViewModels
│ ├── ViewModel3A.swift
│ ├── ViewModel3B.swift
│ ├── ViewModel3C.swift
│ ├── ViewModel3D.swift
│ └── ViewModel3E.swift
│ ├── Views
│ ├── View3D.swift
│ ├── View3A.swift
│ ├── View3C.swift
│ ├── View3B.swift
│ └── View3E.swift
│ └── TestFeature3Module.swift
├── Supporting files
└── NavigationRouter-Info.plist
├── NavigationRouter.podspec
├── LICENSE
└── .gitignore
/TestApp/TestApp/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - Tests/
3 | - TestApp/
4 | - TestFeature1/
5 | - TestFeature2/
6 | - TestFeature3/
7 | - Package.swift
--------------------------------------------------------------------------------
/TestApp/TestApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/NavigationRouter.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/NavigationRouter.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/NavigationRouter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/TestApp/NavigationRouterTestApp.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.network.client
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/NavigationRouter.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
12 |
13 |
15 |
16 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/Helpers/TransitionAnimation.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 | /// Navigation transition animation
24 | public enum NavigationTransition {
25 | /// Left
26 | case left
27 |
28 | /// Right
29 | case right
30 |
31 | /// Up
32 | // swiftlint:disable:next identifier_name missing_docs
33 | case up
34 |
35 | /// Down
36 | case down
37 |
38 | /// None
39 | case none
40 | }
41 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/Code/Models/NavigationRoute.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 route
26 | public struct NavigationRoute: Route {
27 | /// Path
28 | public var path: String
29 |
30 | /// Whether the route requires authentication or not, defaults to true
31 | public var requiresAuthentication: Bool
32 |
33 | /// View
34 | public var type: Routable.Type
35 |
36 | /// Whether the route is allowed externally or not
37 | public var allowedExternally: Bool
38 |
39 | // MARK: - Initializers
40 |
41 | /// Initializes a new instance with given data
42 | /// - Parameters:
43 | /// - path: Path for navigation route (with wildcards for parameters)
44 | /// - type: Any instance conforming to Routable
45 | /// - requiresAuthentication: Whether the route requires authentication or not
46 | /// - allowedExternally: Whether the route is allowed ot be launched externally or not
47 | public init(path: String,
48 | type: Routable.Type,
49 | requiresAuthentication: Bool = true,
50 | allowedExternally: Bool = false) {
51 | self.path = path.lowercased()
52 | self.type = type
53 | self.requiresAuthentication = requiresAuthentication
54 | self.allowedExternally = allowedExternally
55 | }
56 |
57 | // MARK: - Equatable
58 |
59 | /// Gets whether two given instances are equal or not
60 | /// - Parameters:
61 | /// - lhs: First instance to compare
62 | /// - rhs: Second instance to compare
63 | public static func == (lhs: NavigationRoute, rhs: NavigationRoute) -> 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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..: 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------