├── .gitignore ├── .jazzy.yaml ├── .travis.yml ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── XCoordinator │ ├── Animation.swift │ ├── AnyCoordinator.swift │ ├── AnyTransitionPerformer.swift │ ├── BaseCoordinator.swift │ ├── BasicCoordinator.swift │ ├── Container.swift │ ├── Coordinator.swift │ ├── CoordinatorPreviewingDelegateObject.swift │ ├── DeepLinking.swift │ ├── GestureRecognizerTarget.swift │ ├── InteractiveTransitionAnimation.swift │ ├── InterruptibleTransitionAnimation.swift │ ├── NavigationAnimationDelegate.swift │ ├── NavigationCoordinator.swift │ ├── NavigationTransition.swift │ ├── PageCoordinator.swift │ ├── PageCoordinatorDataSource.swift │ ├── PageTransition.swift │ ├── Presentable.swift │ ├── RedirectionRouter.swift │ ├── Route.swift │ ├── Router.swift │ ├── SplitCoordinator.swift │ ├── SplitTransition.swift │ ├── StaticTransitionAnimation.swift │ ├── StrongRouter.swift │ ├── TabBarAnimationDelegate.swift │ ├── TabBarCoordinator.swift │ ├── TabBarTransition.swift │ ├── Transition+Init.swift │ ├── Transition.swift │ ├── TransitionAnimation.swift │ ├── TransitionOptions.swift │ ├── TransitionPerformer.swift │ ├── TransitionProtocol.swift │ ├── UINavigationController+Transition.swift │ ├── UIPageViewController+Transition.swift │ ├── UITabBarController+Transition.swift │ ├── UIView+Store.swift │ ├── UIViewController+Transition.swift │ ├── UnownedErased+Router.swift │ ├── UnownedErased.swift │ ├── ViewCoordinator.swift │ ├── WeakErased+Router.swift │ └── WeakErased.swift ├── XCoordinatorCombine │ └── Router+Combine.swift ├── XCoordinatorRx │ └── Router+Rx.swift └── ios.xcconfig ├── Tests ├── LinuxMain.swift └── XCoordinatorTests │ ├── AnimationTests.swift │ ├── TestAnimation.swift │ ├── TestRoute.swift │ ├── TransitionTests.swift │ ├── XCTestManifests.swift │ ├── XCText+Extras.swift │ └── XCoordinatorTests.xctestplan ├── XCoordinator.podspec ├── XCoordinator.xcodeproj ├── RxSwift_Info.plist ├── XCoordinatorCombine_Info.plist ├── XCoordinatorRx_Info.plist ├── XCoordinatorTests_Info.plist ├── XCoordinator_Info.plist ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── XCoordinator-Package.xcscheme │ ├── XCoordinator.xcscheme │ ├── XCoordinatorCombine.xcscheme │ ├── XCoordinatorRx.xcscheme │ └── XCoordinatorTests.xcscheme ├── docs ├── Classes.html ├── Classes │ ├── Animation.html │ ├── AnyCoordinator.html │ ├── AnyTransitionPerformer.html │ ├── BaseCoordinator.html │ ├── BasicCoordinator.html │ ├── BasicCoordinator │ │ └── InitialLoadingType.html │ ├── InteractiveTransitionAnimation.html │ ├── InterruptibleTransitionAnimation.html │ ├── NavigationAnimationDelegate.html │ ├── NavigationCoordinator.html │ ├── PageCoordinator.html │ ├── PageCoordinatorDataSource.html │ ├── RedirectionRouter.html │ ├── SplitCoordinator.html │ ├── StaticTransitionAnimation.html │ ├── StrongRouter.html │ ├── TabBarAnimationDelegate.html │ ├── TabBarCoordinator.html │ └── ViewCoordinator.html ├── Extensions.html ├── Extensions │ ├── UIView.html │ └── UIViewController.html ├── Protocols.html ├── Protocols │ ├── Container.html │ ├── Coordinator.html │ ├── PercentDrivenInteractionController.html │ ├── Presentable.html │ ├── Router.html │ ├── TransitionAnimation.html │ ├── TransitionContext.html │ ├── TransitionPerformer.html │ └── TransitionProtocol.html ├── Structs.html ├── Structs │ ├── Transition.html │ ├── TransitionOptions.html │ ├── UnownedErased.html │ └── WeakErased.html ├── Typealiases.html ├── css │ ├── highlight.css │ └── jazzy.css ├── docsets │ └── XCoordinator.docset │ │ ├── Contents │ │ ├── Info.plist │ │ └── Resources │ │ │ ├── Documents │ │ │ ├── Classes.html │ │ │ ├── Classes │ │ │ │ ├── Animation.html │ │ │ │ ├── AnyCoordinator.html │ │ │ │ ├── AnyTransitionPerformer.html │ │ │ │ ├── BaseCoordinator.html │ │ │ │ ├── BasicCoordinator.html │ │ │ │ ├── BasicCoordinator │ │ │ │ │ └── InitialLoadingType.html │ │ │ │ ├── InteractiveTransitionAnimation.html │ │ │ │ ├── InterruptibleTransitionAnimation.html │ │ │ │ ├── NavigationAnimationDelegate.html │ │ │ │ ├── NavigationCoordinator.html │ │ │ │ ├── PageCoordinator.html │ │ │ │ ├── PageCoordinatorDataSource.html │ │ │ │ ├── RedirectionRouter.html │ │ │ │ ├── SplitCoordinator.html │ │ │ │ ├── StaticTransitionAnimation.html │ │ │ │ ├── StrongRouter.html │ │ │ │ ├── TabBarAnimationDelegate.html │ │ │ │ ├── TabBarCoordinator.html │ │ │ │ └── ViewCoordinator.html │ │ │ ├── Extensions.html │ │ │ ├── Extensions │ │ │ │ ├── UIView.html │ │ │ │ └── UIViewController.html │ │ │ ├── Protocols.html │ │ │ ├── Protocols │ │ │ │ ├── Container.html │ │ │ │ ├── Coordinator.html │ │ │ │ ├── PercentDrivenInteractionController.html │ │ │ │ ├── Presentable.html │ │ │ │ ├── Router.html │ │ │ │ ├── TransitionAnimation.html │ │ │ │ ├── TransitionContext.html │ │ │ │ ├── TransitionPerformer.html │ │ │ │ └── TransitionProtocol.html │ │ │ ├── Structs.html │ │ │ ├── Structs │ │ │ │ ├── Transition.html │ │ │ │ ├── TransitionOptions.html │ │ │ │ ├── UnownedErased.html │ │ │ │ └── WeakErased.html │ │ │ ├── Typealiases.html │ │ │ ├── css │ │ │ │ ├── highlight.css │ │ │ │ └── jazzy.css │ │ │ ├── img │ │ │ │ ├── carat.png │ │ │ │ ├── dash.png │ │ │ │ ├── gh.png │ │ │ │ └── spinner.gif │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── jazzy.js │ │ │ │ ├── jazzy.search.js │ │ │ │ ├── jquery.min.js │ │ │ │ ├── lunr.min.js │ │ │ │ └── typeahead.jquery.js │ │ │ └── search.json │ │ │ └── docSet.dsidx │ │ └── icon.png ├── img │ ├── carat.png │ ├── dash.png │ ├── gh.png │ └── spinner.gif ├── index.html ├── js │ ├── jazzy.js │ ├── jazzy.search.js │ ├── jquery.min.js │ ├── lunr.min.js │ └── typeahead.jquery.js ├── search.json └── undocumented.json └── scripts ├── build.sh ├── check_docs.sh └── docs.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | *.DS_Store 3 | 4 | # Xcode 5 | .build 6 | *.pbxuser 7 | *.mode1v3 8 | *.mode2v3 9 | *.perspectivev3 10 | *.xcuserstate 11 | project.xcworkspace/ 12 | xcuserdata/ 13 | Pods/*.xcodeproj/xcuserdata/ 14 | 15 | # Generated files 16 | *.o 17 | *.pyc 18 | 19 | # Docs 20 | docs/docsets/XCoordinator.tgz 21 | docs/undocumented.json 22 | 23 | # Backup files 24 | *~.nib 25 | \#*# 26 | .#* 27 | 28 | .swiftpm 29 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | author: "Stefan Kofler & Paul Kraft" 2 | author_url: https://quickbirdstudios.com 3 | podspec: XCoordinator.podspec 4 | docset_icon: Images/logo-single.png 5 | github_url: https://github.com/quickbirdstudios/XCoordinator 6 | hide_documentation_coverage: true 7 | theme: fullwidth 8 | clean: true 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode11 3 | 4 | install: 5 | # - gem update 6 | # - gem install jazzy 7 | # - brew update 8 | # - brew install sourcekitten 9 | script: 10 | - cd scripts 11 | - ./build.sh 12 | # - ./check_docs.sh 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 QuickBird Studios 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 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "RxSwift", 6 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "7c17a6ccca06b5c107cfa4284e634562ddaf5951", 10 | "version": "6.2.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "XCoordinator", 8 | platforms: [.iOS(.v9), .tvOS(.v9)], 9 | products: [ 10 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 11 | .library( 12 | name: "XCoordinator", 13 | targets: ["XCoordinator"]), 14 | .library( 15 | name: "XCoordinatorRx", 16 | targets: ["XCoordinatorRx"]), 17 | .library( 18 | name: "XCoordinatorCombine", 19 | targets: ["XCoordinatorCombine"]), 20 | ], 21 | dependencies: [ 22 | // Dependencies declare other packages that this package depends on. 23 | // .package(url: /* package url */, from: "1.0.0"), 24 | .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.0.0"), 25 | ], 26 | targets: [ 27 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 28 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 29 | .target( 30 | name: "XCoordinator", 31 | dependencies: []), 32 | .target( 33 | name: "XCoordinatorRx", 34 | dependencies: ["XCoordinator", "RxSwift"]), 35 | .target( 36 | name: "XCoordinatorCombine", 37 | dependencies: ["XCoordinator"]), 38 | .testTarget( 39 | name: "XCoordinatorTests", 40 | dependencies: ["XCoordinator", "XCoordinatorRx"]), 41 | ] 42 | ) 43 | -------------------------------------------------------------------------------- /Sources/XCoordinator/Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Animation.swift 3 | // XCoordinator 4 | // 5 | // Created by Stefan Kofler on 03.05.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// `Animation` is used to set presentation and dismissal animations for presentables. 13 | /// 14 | /// Depending on the transition in use, different properties of a `UIViewController` are set to make sure the transition animation is used. 15 | /// 16 | /// - Note: 17 | /// To not override the previously set `Animation`, use `nil` when initializing a transition. 18 | /// 19 | /// Make sure to hold a strong reference to the `Animation` object, as it is only held by a weak reference. 20 | /// 21 | open class Animation: NSObject { 22 | 23 | // MARK: Static properties 24 | 25 | /// 26 | /// Use `Animation.default` to override currently set animations 27 | /// and reset to the default animations provided by iOS 28 | /// 29 | /// - Note: 30 | /// To disable animations make sure to use non-animating `TransitionOptions` when triggering. 31 | /// 32 | public static let `default` = Animation(presentation: nil, dismissal: nil) 33 | 34 | // MARK: Stored properties 35 | 36 | /// The transition animation performed when transitioning to a presentable. 37 | open var presentationAnimation: TransitionAnimation? 38 | 39 | /// The transition animation performed when transitioning away from a presentable. 40 | open var dismissalAnimation: TransitionAnimation? 41 | 42 | // MARK: Initialization 43 | 44 | /// 45 | /// Creates an Animation object containing a presentation and a dismissal animation. 46 | /// 47 | /// - Parameters: 48 | /// - presentation: The transition animation performed when transitioning to a presentable. 49 | /// - dismissal: The transition animation performed when transitioning away from a presentable. 50 | /// 51 | public init(presentation: TransitionAnimation?, dismissal: TransitionAnimation?) { 52 | self.presentationAnimation = presentation 53 | self.dismissalAnimation = dismissal 54 | } 55 | 56 | } 57 | 58 | // MARK: - UIViewControllerTransitioningDelegate 59 | 60 | extension Animation: UIViewControllerTransitioningDelegate { 61 | 62 | /// 63 | /// See [UIViewControllerTransitioningDelegate](https://developer.apple.com/documentation/uikit/UIViewControllerTransitioningDelegate) 64 | /// for further reference. 65 | /// 66 | /// - Parameters: 67 | /// - presented: The view controller to be presented. 68 | /// - presenting: The view controller that is presenting. 69 | /// - source: The view controller whose `present(_:animated:completion:)` was called. 70 | /// 71 | /// - Returns: 72 | /// The presentation animation when initializing the `Animation` object. 73 | /// 74 | open func animationController(forPresented presented: UIViewController, 75 | presenting: UIViewController, 76 | source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 77 | presentationAnimation 78 | } 79 | 80 | /// 81 | /// See [UIViewControllerTransitioningDelegate](https://developer.apple.com/documentation/uikit/UIViewControllerTransitioningDelegate) 82 | /// for further reference. 83 | /// 84 | /// - Parameter dismissed: 85 | /// The view controller to be dismissed. 86 | /// 87 | /// - Returns: 88 | /// The dismissal animation when initializing the `Animation` object. 89 | /// 90 | open func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 91 | dismissalAnimation 92 | } 93 | 94 | /// 95 | /// See [UIViewControllerTransitioningDelegate](https://developer.apple.com/documentation/uikit/UIViewControllerTransitioningDelegate) 96 | /// for further reference. 97 | /// 98 | /// - Parameter animator: 99 | /// The animator of this transition, which is most likely the presentation animation. 100 | /// 101 | /// - Returns: 102 | /// The presentation animation's interaction controller. 103 | /// 104 | open func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) 105 | -> UIViewControllerInteractiveTransitioning? { 106 | presentationAnimation?.interactionController 107 | } 108 | 109 | /// 110 | /// See [UIViewControllerTransitioningDelegate](https://developer.apple.com/documentation/uikit/UIViewControllerTransitioningDelegate) 111 | /// for further reference. 112 | /// 113 | /// - Parameter animator: 114 | /// The animator of this transition, which is most likely the dismissal animation. 115 | /// 116 | /// - Returns: 117 | /// The dismissal animation's interaction controller. 118 | /// 119 | open func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) 120 | -> UIViewControllerInteractiveTransitioning? { 121 | dismissalAnimation?.interactionController 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /Sources/XCoordinator/AnyCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyCoordinator.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 25.10.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A type-erased Coordinator (`AnyCoordinator`) with a `UINavigationController` as rootViewController. 12 | public typealias AnyNavigationCoordinator = AnyCoordinator 13 | 14 | /// A type-erased Coordinator (`AnyCoordinator`) with a `UITabBarController` as rootViewController. 15 | public typealias AnyTabBarCoordinator = AnyCoordinator 16 | 17 | /// A type-erased Coordinator (`AnyCoordinator`) with a `UIViewController` as rootViewController. 18 | public typealias AnyViewCoordinator = AnyCoordinator 19 | 20 | /// 21 | /// `AnyCoordinator` is a type-erased `Coordinator` (`RouteType` & `TransitionType`) and 22 | /// can be used as an abstraction from a specific coordinator class while still specifying 23 | /// TransitionType and RouteType. 24 | /// 25 | /// - Note: 26 | /// If you do not want/need to specify TransitionType, you might want to look into the 27 | /// different router abstractions `StrongRouter`, `UnownedRouter` and `WeakRouter`. 28 | /// See `AnyTransitionPerformer` to further abstract from RouteType. 29 | /// 30 | public class AnyCoordinator: Coordinator { 31 | 32 | // MARK: Stored properties 33 | 34 | private let _prepareTransition: (RouteType) -> TransitionType 35 | private let _viewController: () -> UIViewController? 36 | private let _rootViewController: () -> TransitionType.RootViewController 37 | private let _presented: (Presentable?) -> Void 38 | private let _setRoot: (UIWindow) -> Void 39 | private let _addChild: (Presentable) -> Void 40 | private let _removeChild: (Presentable) -> Void 41 | private let _removeChildrenIfNeeded: () -> Void 42 | private let _registerParent: (Presentable & AnyObject) -> Void 43 | 44 | // MARK: Initialization 45 | 46 | /// 47 | /// Creates a type-erased Coordinator for a specific coordinator. 48 | /// 49 | /// A strong reference to the source coordinator is kept. 50 | /// 51 | /// - Parameter coordinator: 52 | /// The source coordinator. 53 | /// 54 | public init(_ coordinator: C) where C.RouteType == RouteType, C.TransitionType == TransitionType { 55 | self._prepareTransition = coordinator.prepareTransition 56 | self._viewController = { coordinator.viewController } 57 | self._rootViewController = { coordinator.rootViewController } 58 | self._presented = coordinator.presented 59 | self._setRoot = coordinator.setRoot 60 | self._addChild = coordinator.addChild 61 | self._removeChild = coordinator.removeChild 62 | self._removeChildrenIfNeeded = coordinator.removeChildrenIfNeeded 63 | self._registerParent = coordinator.registerParent 64 | } 65 | 66 | // MARK: Computed properties 67 | 68 | public var rootViewController: TransitionType.RootViewController { 69 | _rootViewController() 70 | } 71 | 72 | public var viewController: UIViewController! { 73 | _viewController() 74 | } 75 | 76 | // MARK: Methods 77 | 78 | /// 79 | /// Prepare and return transitions for a given route. 80 | /// 81 | /// - Parameter route: 82 | /// The triggered route for which a transition is to be prepared. 83 | /// 84 | /// - Returns: 85 | /// The prepared transition. 86 | /// 87 | public func prepareTransition(for route: RouteType) -> TransitionType { 88 | _prepareTransition(route) 89 | } 90 | 91 | public func presented(from presentable: Presentable?) { 92 | _presented(presentable) 93 | } 94 | 95 | public func registerParent(_ presentable: Presentable & AnyObject) { 96 | _registerParent(presentable) 97 | } 98 | 99 | public func setRoot(for window: UIWindow) { 100 | _setRoot(window) 101 | } 102 | 103 | public func addChild(_ presentable: Presentable) { 104 | _addChild(presentable) 105 | } 106 | 107 | public func removeChild(_ presentable: Presentable) { 108 | _removeChild(presentable) 109 | } 110 | 111 | public func removeChildrenIfNeeded() { 112 | _removeChildrenIfNeeded() 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /Sources/XCoordinator/AnyTransitionPerformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyTransitionPerformer.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 13.09.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// AnyTransitionPerformer can be used as an abstraction from a specific TransitionPerformer implementation 13 | /// without losing type information about its TransitionType. 14 | /// 15 | /// This type abstraction can be especially helpful when performing transitions. 16 | /// AnyTransitionPerformer abstracts away any implementation specific details and reduces coordinators to the capabilities 17 | /// of the `TransitionPerformer` protocol. 18 | /// 19 | public class AnyTransitionPerformer: TransitionPerformer { 20 | 21 | // MARK: Stored properties 22 | 23 | private var _viewController: () -> UIViewController? 24 | private var _rootViewController: () -> TransitionType.RootViewController 25 | private var _presented: (Presentable?) -> Void 26 | private var _perform: (TransitionType, TransitionOptions, PresentationHandler?) -> Void 27 | 28 | // MARK: Computed properties 29 | 30 | public var viewController: UIViewController! { 31 | _viewController() 32 | } 33 | 34 | public var rootViewController: TransitionType.RootViewController { 35 | _rootViewController() 36 | } 37 | 38 | // MARK: Methods 39 | 40 | public func presented(from presentable: Presentable?) { 41 | _presented(presentable) 42 | } 43 | 44 | public func performTransition(_ transition: TransitionType, 45 | with options: TransitionOptions, 46 | completion: PresentationHandler? = nil) { 47 | _perform(transition, options, completion) 48 | } 49 | 50 | // MARK: Initialization 51 | 52 | init(_ coordinator: T) where TransitionType == T.TransitionType { 53 | self._viewController = { coordinator.viewController } 54 | self._presented = coordinator.presented 55 | self._rootViewController = { coordinator.rootViewController } 56 | self._perform = coordinator.performTransition 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/XCoordinator/BasicCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicCoordinator.swift 3 | // XCoordinator 4 | // 5 | // Created by Stefan Kofler on 05.05.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | /// A BasicCoordinator with a `UINavigationController` as its rootViewController. 10 | public typealias BasicNavigationCoordinator = BasicCoordinator 11 | 12 | /// A BasicCoordinator with a `UIViewController` as its rootViewController. 13 | public typealias BasicViewCoordinator = BasicCoordinator 14 | 15 | /// A BasicCoordinator with a `UITabBarController` as its rootViewController. 16 | public typealias BasicTabBarCoordinator = BasicCoordinator 17 | 18 | /// 19 | /// BasicCoordinator is a coordinator class that can be used without subclassing. 20 | /// 21 | /// Although subclassing of coordinators is encouraged for more complex cases, a `BasicCoordinator` can easily 22 | /// be created by only providing a `prepareTransition` closure, an `initialRoute` and an `initialLoadingType`. 23 | /// 24 | open class BasicCoordinator: BaseCoordinator { 25 | 26 | // MARK: Nested types 27 | 28 | /// 29 | /// `InitialLoadingType` differentiates between different points in time when the initital route is to 30 | /// be triggered by the coordinator. 31 | /// 32 | public enum InitialLoadingType { 33 | 34 | /// The initial route is triggered before the coordinator is made visible (i.e. on initialization). 35 | case immediately 36 | 37 | /// The initial route is triggered after the coordinator is made visible. 38 | case presented 39 | } 40 | 41 | // MARK: Stored properties 42 | 43 | private let initialRoute: RouteType? 44 | private let initialLoadingType: InitialLoadingType 45 | private let prepareTransition: ((RouteType) -> TransitionType)? 46 | 47 | // MARK: Initialization 48 | 49 | /// 50 | /// Creates a BasicCoordinator. 51 | /// 52 | /// - Parameters: 53 | /// - initialRoute: 54 | /// If a route is specified, it is triggered depending on the initialLoadingType. 55 | /// - initialLoadingType: 56 | /// The initialLoadingType specifies when the initialRoute is triggered. 57 | /// - prepareTransition: 58 | /// A closure to define transitions based on triggered routes. 59 | /// Make sure to override `prepareTransition` by subclassing, if you specify `nil` here. 60 | /// 61 | /// - Seealso: 62 | /// See `InitialLoadingType` for more information. 63 | /// 64 | public init(rootViewController: RootViewController, 65 | initialRoute: RouteType? = nil, 66 | initialLoadingType: InitialLoadingType = .presented, 67 | prepareTransition: ((RouteType) -> TransitionType)?) { 68 | self.initialRoute = initialRoute 69 | self.initialLoadingType = initialLoadingType 70 | self.prepareTransition = prepareTransition 71 | 72 | if initialLoadingType == .immediately { 73 | super.init(rootViewController: rootViewController, initialRoute: initialRoute) 74 | } else { 75 | super.init(rootViewController: rootViewController, initialRoute: nil) 76 | } 77 | } 78 | 79 | // MARK: Open methods 80 | 81 | /// 82 | /// This method is called whenever the BasicCoordinator is shown to the user. 83 | /// 84 | /// If `initialLoadingType` has been specified as `presented` and an initialRoute is present, 85 | /// the route is triggered here. 86 | /// 87 | /// - Parameter presentable: 88 | /// The context in which this coordinator has been shown to the user. 89 | /// 90 | open override func presented(from presentable: Presentable?) { 91 | super.presented(from: presentable) 92 | 93 | if let initialRoute = initialRoute, initialLoadingType == .presented { 94 | trigger(initialRoute, with: TransitionOptions(animated: false), completion: nil) 95 | } 96 | } 97 | 98 | open override func prepareTransition(for route: RouteType) -> TransitionType { 99 | if let prepareTransition = prepareTransition { 100 | return prepareTransition(route) 101 | } else { 102 | fatalError("Either pass a \(#function) closure to the initializer or override this method.") 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/XCoordinator/Container.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Container.swift 3 | // XCoordinator 4 | // 5 | // Created by Stefan Kofler on 30.04.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// Container abstracts away from the difference of `UIView` and `UIViewController` 13 | /// 14 | /// With the Container protocol, `UIView` and `UIViewController` objects can be used interchangeably, 15 | /// e.g. when embedding containers into containers. 16 | /// 17 | public protocol Container { 18 | 19 | /// 20 | /// The view of the Container. 21 | /// 22 | /// - Note: 23 | /// It might not exist for a `UIViewController`. 24 | /// 25 | var view: UIView! { get } 26 | 27 | /// 28 | /// The viewController of the Container. 29 | /// 30 | /// - Note: 31 | /// It might not exist for a `UIView`. 32 | /// 33 | var viewController: UIViewController! { get } 34 | } 35 | 36 | // MARK: - Extensions 37 | 38 | extension UIViewController: Container { 39 | public var viewController: UIViewController! { self } 40 | } 41 | 42 | extension UIView: Container { 43 | public var viewController: UIViewController! { 44 | viewController(for: self) 45 | } 46 | 47 | public var view: UIView! { self } 48 | } 49 | 50 | extension UIView { 51 | private func viewController(for responder: UIResponder) -> UIViewController? { 52 | if let viewController = responder as? UIViewController { 53 | return viewController 54 | } 55 | 56 | if let nextResponser = responder.next { 57 | return viewController(for: nextResponser) 58 | } 59 | 60 | return nil 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/XCoordinator/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // XCoordinator 4 | // 5 | // Created by Stefan Kofler on 30.04.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The completion handler for transitions. 12 | public typealias PresentationHandler = () -> Void 13 | 14 | /// The completion handler for transitions, which also provides the context information about the transition. 15 | public typealias ContextPresentationHandler = (TransitionContext) -> Void 16 | 17 | /// 18 | /// Coordinator is the protocol every coordinator conforms to. 19 | /// 20 | /// It requires an object to be able to trigger routes and perform transitions. 21 | /// This connection is created using the `prepareTransition(for:)` method. 22 | /// 23 | public protocol Coordinator: Router, TransitionPerformer { 24 | 25 | /// 26 | /// This method prepares transitions for routes. 27 | /// It especially decides, which transitions are performed for the triggered routes. 28 | /// 29 | /// - Parameter route: 30 | /// The triggered route for which a transition is to be prepared. 31 | /// 32 | /// - Returns: 33 | /// The prepared transition. 34 | /// 35 | func prepareTransition(for route: RouteType) -> TransitionType 36 | 37 | /// 38 | /// This method adds a child to a coordinator's children. 39 | /// 40 | /// - Parameter presentable: 41 | /// The child to be added. 42 | /// 43 | func addChild(_ presentable: Presentable) 44 | 45 | /// 46 | /// This method removes a child to a coordinator's children. 47 | /// 48 | /// - Parameter presentable: 49 | /// The child to be removed. 50 | /// 51 | func removeChild(_ presentable: Presentable) 52 | 53 | /// This method removes all children that are no longer in the view hierarchy. 54 | func removeChildrenIfNeeded() 55 | } 56 | 57 | // MARK: - Typealiases 58 | 59 | extension Coordinator { 60 | 61 | /// Shortcut for Coordinator.TransitionType.RootViewController 62 | public typealias RootViewController = TransitionType.RootViewController 63 | } 64 | 65 | // MARK: - Presentable 66 | 67 | extension Coordinator { 68 | 69 | /// A Coordinator uses its rootViewController as viewController. 70 | public var viewController: UIViewController! { 71 | rootViewController 72 | } 73 | } 74 | 75 | extension Coordinator where Self: AnyObject { 76 | 77 | /// 78 | /// Creates a WeakRouter object from the given router to abstract from concrete implementations 79 | /// while maintaining information necessary to fulfill the Router protocol. 80 | /// The original router will be held weakly. 81 | /// 82 | public var weakRouter: WeakRouter { 83 | WeakRouter(self) { $0.strongRouter } 84 | } 85 | 86 | /// 87 | /// Creates an UnownedRouter object from the given router to abstract from concrete implementations 88 | /// while maintaining information necessary to fulfill the Router protocol. 89 | /// The original router will be held unowned. 90 | /// 91 | 92 | public var unownedRouter: UnownedRouter { 93 | UnownedRouter(self) { $0.strongRouter } 94 | } 95 | 96 | } 97 | 98 | // MARK: - Default implementations 99 | 100 | extension Coordinator where Self: AnyObject { 101 | 102 | /// Creates an AnyCoordinator based on the current coordinator. 103 | public var anyCoordinator: AnyCoordinator { 104 | AnyCoordinator(self) 105 | } 106 | 107 | public func presented(from presentable: Presentable?) {} 108 | 109 | public func childTransitionCompleted() { 110 | removeChildrenIfNeeded() 111 | } 112 | 113 | public func contextTrigger(_ route: RouteType, 114 | with options: TransitionOptions, 115 | completion: ContextPresentationHandler?) { 116 | let transition = prepareTransition(for: route) 117 | performTransition(transition, with: options) { completion?(transition) } 118 | } 119 | 120 | /// 121 | /// With `chain(routes:)` different routes can be chained together to form a combined transition. 122 | /// 123 | /// - Parameter routes: 124 | /// The routes to be chained. 125 | /// 126 | /// - Returns: 127 | /// A transition combining the transitions of the specified routes. 128 | /// 129 | public func chain(routes: [RouteType]) -> TransitionType { 130 | .multiple(routes.map(prepareTransition)) 131 | } 132 | 133 | public func performTransition(_ transition: TransitionType, 134 | with options: TransitionOptions, 135 | completion: PresentationHandler? = nil) { 136 | transition.presentables.forEach(addChild) 137 | transition.perform(on: rootViewController, with: options) { 138 | completion?() 139 | self.removeChildrenIfNeeded() 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/XCoordinator/CoordinatorPreviewingDelegateObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinatorPreviewingDelegateObject.swift 3 | // XCoordinator 4 | // 5 | // Created by Stefan Kofler on 19.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal class CoordinatorPreviewingDelegateObject: 12 | NSObject, UIViewControllerPreviewingDelegate { 13 | 14 | // MARK: Stored properties 15 | 16 | internal var context: UIViewControllerPreviewing? 17 | 18 | private weak var viewController: UIViewController? 19 | 20 | private let transition: () -> TransitionType 21 | private let rootViewController: TransitionType.RootViewController 22 | private let completion: PresentationHandler? 23 | 24 | // MARK: Initialization 25 | 26 | internal init(transition: @escaping () -> TransitionType, 27 | rootViewController: TransitionType.RootViewController, 28 | completion: PresentationHandler?) { 29 | self.transition = transition 30 | self.rootViewController = rootViewController 31 | self.completion = completion 32 | } 33 | 34 | // MARK: Methods 35 | 36 | internal func previewingContext(_ previewingContext: UIViewControllerPreviewing, 37 | viewControllerForLocation location: CGPoint) -> UIViewController? { 38 | 39 | if let viewController = viewController { 40 | return viewController 41 | } 42 | 43 | let presentable = transition().presentables.last 44 | presentable?.presented(from: nil) 45 | viewController = presentable?.viewController 46 | return viewController 47 | } 48 | 49 | internal func previewingContext(_ previewingContext: UIViewControllerPreviewing, 50 | commit viewControllerToCommit: UIViewController) { 51 | transition().perform(on: rootViewController, with: .default, completion: completion) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/XCoordinator/DeepLinking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeepLinking.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 30.10.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | /// 10 | /// `TransitionContext` provides context information about transitions. 11 | /// 12 | /// It is especially useful for deep linking as XCoordinator can internally gather information about 13 | /// the presentables being pushed onto the view hierarchy. 14 | /// 15 | public protocol TransitionContext { 16 | 17 | /// The presentables being shown to the user by the transition. 18 | var presentables: [Presentable] { get } 19 | 20 | /// 21 | /// The transition animation directly used in the transition, if applicable. 22 | /// 23 | /// - Note: 24 | /// Make sure to not return `nil`, if you want to use `BaseCoordinator.registerInteractiveTransition` 25 | /// to realize an interactive transition. 26 | /// 27 | var animation: TransitionAnimation? { get } 28 | } 29 | 30 | // MARK: - Coordinator + DeepLinking 31 | 32 | extension Coordinator where Self: AnyObject { 33 | 34 | /// 35 | /// Deep-Linking can be used to chain routes of different types together. 36 | /// 37 | /// - Parameters: 38 | /// - route: 39 | /// The first route in the chain. 40 | /// It is given a special place because its exact type can be specified. 41 | /// - remainingRoutes: 42 | /// The remaining routes of the chain. 43 | /// 44 | /// - Note: 45 | /// Use it with caution, as it is not implemented in a type-safe manner. 46 | /// Keep in mind that changes in the app's structure and changes of transitions 47 | /// behind the given routes can lead to runtime errors and, therefore, crashes of your app. 48 | /// 49 | public func deepLink(_ route: RouteType, _ remainingRoutes: S) 50 | -> Transition where S.Element == Route, TransitionType == Transition { 51 | .deepLink(with: self, route, array: Array(remainingRoutes)) 52 | } 53 | 54 | /// 55 | /// Deep-Linking can be used to chain routes of different types together. 56 | /// 57 | /// - Parameters 58 | /// - route: 59 | /// The first route in the chain. 60 | /// It is given a special place because its exact type can be specified. 61 | /// - remainingRoutes: 62 | /// The remaining routes of the chain. 63 | /// As it is not implemented in a type-safe manner, use it with caution. 64 | /// Keep in mind that changes in the app's structure and changes of transitions 65 | /// behind the given routes can lead to runtime errors and, therefore, crashes of your app. 66 | /// 67 | public func deepLink(_ route: RouteType, _ remainingRoutes: Route...) 68 | -> Transition where TransitionType == Transition { 69 | .deepLink(with: self, route, array: remainingRoutes) 70 | } 71 | } 72 | 73 | // MARK: - Transition + DeepLink 74 | 75 | extension Transition { 76 | fileprivate static func deepLink(with coordinator: C, 77 | _ route: C.RouteType, 78 | array remainingRoutes: [Route]) -> Transition { 79 | 80 | Transition(presentables: [], animationInUse: nil) { [weak coordinator] _, options, completion in 81 | guard let coordinator = coordinator else { 82 | assertionFailure("Please use the coordinator responsible for executing a deepLink-Transition when initializing.") 83 | completion?() 84 | return 85 | } 86 | 87 | route.trigger(on: [coordinator], remainingRoutes: ArraySlice(remainingRoutes), 88 | with: options, completion: completion) 89 | } 90 | } 91 | } 92 | 93 | // MARK: - Route + DeepLink 94 | 95 | extension Route { 96 | private func router(fromStack stack: inout [Presentable]) -> StrongRouter? { 97 | while !stack.isEmpty { 98 | if let router = stack.last?.router(for: self) { 99 | return router 100 | } 101 | stack.removeLast() 102 | } 103 | return nil 104 | } 105 | 106 | fileprivate func trigger(on presentables: [Presentable], 107 | remainingRoutes: ArraySlice, 108 | with options: TransitionOptions, 109 | completion: PresentationHandler?) { 110 | var stack = presentables 111 | 112 | guard let router = router(fromStack: &stack) else { 113 | // swiftlint:disable:next line_length 114 | assertionFailure("Could not find appropriate router for \(self). The following routes could not be triggered: \([self] + remainingRoutes).") 115 | completion?() 116 | return 117 | } 118 | 119 | router.contextTrigger(self, with: options) { context in 120 | guard let nextRoute = remainingRoutes.first else { 121 | completion?() 122 | return 123 | } 124 | 125 | stack.append(contentsOf: context.presentables) 126 | nextRoute.trigger(on: stack, remainingRoutes: remainingRoutes.dropFirst(), 127 | with: options, completion: completion) 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/XCoordinator/GestureRecognizerTarget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GestureRecognizerTarget.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 19.12.18. 6 | // 7 | 8 | import UIKit 9 | 10 | internal protocol GestureRecognizerTarget { 11 | var gestureRecognizer: UIGestureRecognizer? { get } 12 | } 13 | 14 | internal class Target: GestureRecognizerTarget { 15 | 16 | // MARK: Stored properties 17 | 18 | private let handler: (GestureRecognizer) -> Void 19 | internal private(set) weak var gestureRecognizer: UIGestureRecognizer? 20 | 21 | // MARK: Initialization 22 | 23 | init(recognizer gestureRecognizer: GestureRecognizer, handler: @escaping (GestureRecognizer) -> Void) { 24 | self.handler = handler 25 | self.gestureRecognizer = gestureRecognizer 26 | // The method signature "handle(_ gestureRecognizer: UIGestureRecognizer)" is in conflict with validation Apple, use another name : "handleMyGesture" 27 | gestureRecognizer.addTarget(self, action: #selector(handleGesture(of: ))) 28 | } 29 | 30 | // MARK: Target actions 31 | 32 | @objc 33 | private func handleGesture(of gestureRecognizer: UIGestureRecognizer) { 34 | guard let recognizer = gestureRecognizer as? GestureRecognizer else { return } 35 | handler(recognizer) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/XCoordinator/InteractiveTransitionAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InteractiveTransitionAnimation.swift 3 | // XCoordinator 4 | // 5 | // Created by Joan Disho on 03.05.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // swiftlint:disable line_length 12 | 13 | /// 14 | /// `InteractiveTransitionAnimation` provides a simple interface to create interactive transition animations. 15 | /// 16 | /// An `InteractiveTransitionAnimation` can be created by providing the duration, the animation code 17 | /// and (optionally) a closure to create an interaction controller. 18 | /// 19 | /// - Note: 20 | /// To get further information read the UIKit documentation of 21 | /// [UIViewControllerAnimatedTransitioning](https://developer.apple.com/documentation/uikit/UIViewControllerAnimatedTransitioning), 22 | /// [UIViewControllerInteractiveTransitioning](https://developer.apple.com/documentation/uikit/UIViewControllerInteractiveTransitioning), 23 | /// [UIViewControllerContextTransitioning](https://developer.apple.com/documentation/uikit/UIViewControllerContextTransitioning) and 24 | /// [UIPercentDrivenInteractiveTransition](https://developer.apple.com/documentation/uikit/UIPercentDrivenInteractiveTransition). 25 | /// 26 | open class InteractiveTransitionAnimation: NSObject, TransitionAnimation { // swiftlint:enable line_length 27 | 28 | // MARK: Static properties 29 | 30 | internal static let generateDefaultInteractionController: () -> PercentDrivenInteractionController? = { 31 | UIPercentDrivenInteractiveTransition() 32 | } 33 | 34 | // MARK: Stored properties 35 | 36 | private let _duration: TimeInterval 37 | private let _animation: (UIViewControllerContextTransitioning) -> Void 38 | private var _generateInteractionController: () -> PercentDrivenInteractionController? 39 | 40 | private var _interactionController: PercentDrivenInteractionController? 41 | 42 | // MARK: Computed properties 43 | 44 | open var interactionController: PercentDrivenInteractionController? { 45 | _interactionController 46 | } 47 | 48 | // MARK: Initialization 49 | 50 | /// 51 | /// Creates an InteractiveTransitionAnimation with a duration, an animation closure and a closure to 52 | /// generate an interaction controller. 53 | /// 54 | /// - Parameters 55 | /// - duration: The duration of the animation. 56 | /// - transition: The animation code. 57 | /// - context: The context in which the transition is performed. 58 | /// - generateInteractionController: 59 | /// The closure to generate an interaction controller when needed, 60 | /// usually at the beginning of a transition. 61 | /// 62 | public init(duration: TimeInterval, 63 | transition: @escaping (UIViewControllerContextTransitioning) -> Void, 64 | generateInteractionController: @escaping () -> PercentDrivenInteractionController?) { 65 | self._duration = duration 66 | self._animation = transition 67 | self._generateInteractionController = generateInteractionController 68 | } 69 | 70 | /// 71 | /// Convenience initializer for `init(duration:transition:generateInteractionController:)`. 72 | /// By ommitting the `generateInteractionController` closure, the transition will use 73 | /// [UIPercentDrivenInteractiveTransition](https://developer.apple.com/documentation/uikit/UIPercentDrivenInteractiveTransition) 74 | /// to create interaction controllers. 75 | /// 76 | /// - Parameters: 77 | /// - duration: The duration of the animation. 78 | /// - transition: The animation code. 79 | /// 80 | public convenience init(duration: TimeInterval, 81 | transition: @escaping (UIViewControllerContextTransitioning) -> Void) { 82 | self.init( 83 | duration: duration, 84 | transition: transition, 85 | generateInteractionController: InteractiveTransitionAnimation.generateDefaultInteractionController 86 | ) 87 | } 88 | 89 | /// 90 | /// Convenience initializer for `init(duration:transition:generateInteractionController:)`. 91 | /// Provides a simple interface to convert StaticTransitionAnimations to interactive transition animations. 92 | /// 93 | /// - Parameters: 94 | /// - transitionAnimation: The StaticTransitionAnimation to be converted. 95 | /// - generateInteractionController: 96 | /// The closure to generate an interaction controller when needed, 97 | /// usually at the beginning of a transition. 98 | /// 99 | public convenience init(transitionAnimation: StaticTransitionAnimation, 100 | generateInteractionController: @escaping () -> PercentDrivenInteractionController?) { 101 | self.init( 102 | duration: transitionAnimation.duration, 103 | transition: transitionAnimation.animateTransition, 104 | generateInteractionController: generateInteractionController 105 | ) 106 | } 107 | 108 | /// 109 | /// Convenience initializer for `init(duration:transition:)`. 110 | /// Provides a simple interface to convert StaticTransitionAnimations to interactive transition animations. 111 | /// 112 | /// - Parameter transitionAnimation: 113 | /// The StaticTransitionAnimation to be converted. 114 | /// 115 | public convenience init(transitionAnimation: StaticTransitionAnimation) { 116 | self.init( 117 | duration: transitionAnimation.duration, 118 | transition: transitionAnimation.animateTransition 119 | ) 120 | } 121 | 122 | // MARK: Methods 123 | 124 | /// 125 | /// See [UIViewControllerAnimatedTransitioning](https://developer.apple.com/documentation/uikit/UIViewControllerAnimatedTransitioning) 126 | /// for further information. 127 | /// 128 | /// - Parameter transitionContext: 129 | /// The context of the transition. 130 | /// 131 | /// - Returns: 132 | /// The transition duration as specified in the initializer. 133 | /// 134 | open func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 135 | _duration 136 | } 137 | 138 | /// 139 | /// See [UIViewControllerAnimatedTransitioning](https://developer.apple.com/documentation/uikit/UIViewControllerAnimatedTransitioning) 140 | /// for further information. 141 | /// 142 | /// - Parameter transitionContext: 143 | /// The context of a transition for which the animation should be started. 144 | /// 145 | open func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 146 | _animation(transitionContext) 147 | } 148 | 149 | // MARK: TransitionAnimation 150 | 151 | /// 152 | /// This method is used to generate an applicable interaction controller. 153 | /// 154 | /// - Note: 155 | /// To allow for more complex logic to create a specific interaction controller, 156 | /// override this method in your subclass. 157 | /// 158 | open func generateInteractionController() -> PercentDrivenInteractionController? { 159 | _generateInteractionController() 160 | } 161 | 162 | /// 163 | /// Starts the transition animation by generating an interaction controller. 164 | /// 165 | open func start() { 166 | _interactionController = generateInteractionController() 167 | } 168 | 169 | /// 170 | /// Ends the transition animation by deleting the interaction controller. 171 | /// 172 | open func cleanup() { 173 | _interactionController = nil 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Sources/XCoordinator/InterruptibleTransitionAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterruptibleTransitionAnimation.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 24.12.18. 6 | // 7 | 8 | import UIKit 9 | 10 | /// 11 | /// Use InterruptibleTransitionAnimation to define interactive transitions based on the 12 | /// [UIViewPropertyAnimator](https://developer.apple.com/documentation/uikit/UIViewPropertyAnimator) 13 | /// APIs introduced in iOS 10. 14 | /// 15 | @available(iOS 10.0, tvOS 10.0, *) 16 | open class InterruptibleTransitionAnimation: InteractiveTransitionAnimation { 17 | 18 | // MARK: Stored properties 19 | 20 | private let _generateInterruptibleAnimator: (UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating 21 | 22 | private var _interruptibleAnimator: UIViewImplicitlyAnimating? 23 | 24 | // MARK: Initialization 25 | 26 | /// 27 | /// Creates an interruptible transition animation based on duration, an animator generator closure 28 | /// and an interaction controller generator closure. 29 | /// 30 | /// - Parameters: 31 | /// - duration: The total duration of the animation. 32 | /// - generateAnimator: A generator closure to create a `UIViewPropertyAnimator` dynamically. 33 | /// - generateInteractionController: 34 | /// A generator closure to create an interaction controller which handles animation progress changes. 35 | /// 36 | public init(duration: TimeInterval, 37 | generateAnimator: @escaping (UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating, 38 | generateInteractionController: @escaping () -> PercentDrivenInteractionController?) { 39 | self._generateInterruptibleAnimator = generateAnimator 40 | super.init( 41 | duration: duration, 42 | transition: { _ in }, 43 | generateInteractionController: generateInteractionController 44 | ) 45 | } 46 | 47 | /// 48 | /// Creates an interruptible transition animation based on duration and an animator generator closure. 49 | /// 50 | /// A `UIPercentDrivenInteractiveTransition` is used as interaction controller. 51 | /// 52 | /// - Parameters: 53 | /// - duration: The total duration of the animation. 54 | /// - generateAnimator: A generator closure to create a `UIViewPropertyAnimator` dynamically. 55 | /// 56 | public convenience init(duration: TimeInterval, 57 | generateAnimator: @escaping (UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating) { 58 | self.init( 59 | duration: duration, 60 | generateAnimator: generateAnimator, 61 | generateInteractionController: InteractiveTransitionAnimation.generateDefaultInteractionController 62 | ) 63 | } 64 | 65 | // MARK: Methods 66 | 67 | /// 68 | /// Generates an interruptible animator based on the transitionContext. 69 | /// It further adds a completion block to the animator to ensure it is deallocated once 70 | /// the transition is finished. 71 | /// 72 | /// This code is called once per transition to generate the interruptible animator 73 | /// which is reused in subsequent calls of `interruptibeAnimator(using:)`. 74 | /// 75 | /// - Parameter transitionContext: 76 | /// The context in which the transition is performed. 77 | /// 78 | open func generateInterruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { 79 | let animator = _generateInterruptibleAnimator(transitionContext) 80 | animator.addCompletion? { [weak self] position in 81 | switch position { 82 | case .start, .end: 83 | self?._interruptibleAnimator = nil 84 | case .current: 85 | break 86 | @unknown default: 87 | break 88 | } 89 | } 90 | _interruptibleAnimator = animator 91 | return animator 92 | } 93 | 94 | // MARK: TransitionAnimation 95 | 96 | /// 97 | /// See [UIViewControllerAnimatedTransitioning](https://developer.apple.com/documentation/uikit/UIViewControllerAnimatedTransitioning) 98 | /// for further information. 99 | /// 100 | /// This method simply calls `startAnimation()` on the interruptible animator. 101 | /// 102 | /// - Parameter transitionContext: 103 | /// The context in which the transition is performed. 104 | /// 105 | open override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 106 | interruptibleAnimator(using: transitionContext).startAnimation() 107 | } 108 | 109 | /// 110 | /// See [UIViewControllerAnimatedTransitioning](https://developer.apple.com/documentation/uikit/UIViewControllerAnimatedTransitioning) 111 | /// for further information. 112 | /// 113 | /// This method returns an already generated interruptible animator, if present. 114 | /// Otherwise it generates a new one using `generateInterruptibleAnimator(using:)`. 115 | /// 116 | /// - Parameter transitionContext: 117 | /// The context in which the transition is performed. 118 | /// 119 | open func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning 120 | ) -> UIViewImplicitlyAnimating { 121 | _interruptibleAnimator ?? generateInterruptibleAnimator(using: transitionContext) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/XCoordinator/NavigationCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationCoordinator.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 29.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// NavigationCoordinator acts as a base class for custom coordinators with a `UINavigationController` 13 | /// as rootViewController. 14 | /// 15 | /// NavigationCoordinator especially ensures that transition animations are called, 16 | /// which would not be the case when creating a `BaseCoordinator`. 17 | /// 18 | open class NavigationCoordinator: BaseCoordinator { 19 | 20 | // MARK: Stored properties 21 | 22 | /// 23 | /// The animation delegate controlling the rootViewController's transition animations. 24 | /// This animation delegate is set to be the rootViewController's rootViewController, if you did not set one earlier. 25 | /// 26 | /// - Note: 27 | /// Use the `delegate` property to set a custom delegate and use transition animations provided by XCoordinator. 28 | /// 29 | public let animationDelegate = NavigationAnimationDelegate() 30 | // swiftlint:disable:previous weak_delegate 31 | 32 | // MARK: Computed properties 33 | 34 | /// 35 | /// This represents a fallback-delegate to be notified about navigation controller events. 36 | /// It is further used to call animation methods when no animation has been specified in the transition. 37 | /// 38 | public var delegate: UINavigationControllerDelegate? { 39 | get { 40 | animationDelegate.delegate 41 | } 42 | set { 43 | animationDelegate.delegate = newValue 44 | } 45 | } 46 | 47 | // MARK: Initialization 48 | 49 | /// 50 | /// Creates a NavigationCoordinator and optionally triggers an initial route. 51 | /// 52 | /// - Parameter initialRoute: 53 | /// The route to be triggered. 54 | /// 55 | public override init(rootViewController: RootViewController = .init(), initialRoute: RouteType? = nil) { 56 | if rootViewController.delegate == nil { 57 | rootViewController.delegate = animationDelegate 58 | } 59 | super.init(rootViewController: rootViewController, initialRoute: initialRoute) 60 | animationDelegate.presentable = self 61 | } 62 | 63 | /// 64 | /// Creates a NavigationCoordinator and pushes a presentable onto the navigation stack right away. 65 | /// 66 | /// - Parameter root: 67 | /// The presentable to be pushed. 68 | /// 69 | public init(rootViewController: RootViewController = .init(), root: Presentable) { 70 | if rootViewController.delegate == nil { 71 | rootViewController.delegate = animationDelegate 72 | } 73 | super.init(rootViewController: rootViewController, initialTransition: .push(root)) 74 | animationDelegate.presentable = self 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Sources/XCoordinator/NavigationTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationTransition.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 27.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// NavigationTransition offers transitions that can be used 12 | /// with a `UINavigationController` as rootViewController. 13 | public typealias NavigationTransition = Transition 14 | 15 | extension Transition where RootViewController: UINavigationController { 16 | 17 | /// 18 | /// Pushes a presentable on the rootViewController's navigation stack. 19 | /// 20 | /// - Parameters: 21 | /// - presentable: The presentable to be pushed onto the navigation stack. 22 | /// - animation: 23 | /// The animation to set for the presentable. Its presentationAnimation will be used for the 24 | /// immediate push-transition, its dismissalAnimation is used for the pop-transition, 25 | /// if not otherwise specified. Specify `nil` here to leave animations as they were set for the 26 | /// presentable before. You can use `Animation.default` to reset the previously set animations 27 | /// on this presentable. 28 | /// 29 | public static func push(_ presentable: Presentable, animation: Animation? = nil) -> Transition { 30 | Transition(presentables: [presentable], 31 | animationInUse: animation?.presentationAnimation 32 | ) { rootViewController, options, completion in 33 | rootViewController.push(presentable.viewController, 34 | with: options, 35 | animation: animation 36 | ) { 37 | presentable.presented(from: rootViewController) 38 | completion?() 39 | } 40 | } 41 | } 42 | 43 | /// 44 | /// Pops the topViewController from the rootViewController's navigation stack. 45 | /// 46 | /// - Parameter animation: 47 | /// The animation to set for the presentable. Only its dismissalAnimation is used for the 48 | /// pop-transition. Specify `nil` here to leave animations as they were set for the 49 | /// presentable before. You can use `Animation.default` to reset the previously set animations 50 | /// on this presentable. 51 | /// 52 | public static func pop(animation: Animation? = nil) -> Transition { 53 | Transition(presentables: [], 54 | animationInUse: animation?.dismissalAnimation 55 | ) { rootViewController, options, completion in 56 | rootViewController.pop(toRoot: false, 57 | with: options, 58 | animation: animation, 59 | completion: completion) 60 | } 61 | } 62 | 63 | /// 64 | /// Pops viewControllers from the rootViewController's navigation stack until the specified 65 | /// presentable is reached. 66 | /// 67 | /// - Parameters: 68 | /// - presentable: 69 | /// The presentable to pop to. Make sure this presentable is in the rootViewController's 70 | /// navigation stack before performing such a transition. 71 | /// - animation: 72 | /// The animation to set for the presentable. Only its dismissalAnimation is used for the 73 | /// pop-transition. Specify `nil` here to leave animations as they were set for the 74 | /// presentable before. You can use `Animation.default` to reset the previously set animations 75 | /// on this presentable. 76 | /// 77 | public static func pop(to presentable: Presentable, animation: Animation? = nil) -> Transition { 78 | Transition(presentables: [presentable], 79 | animationInUse: animation?.dismissalAnimation 80 | ) { rootViewController, options, completion in 81 | rootViewController.pop(to: presentable.viewController, 82 | options: options, 83 | animation: animation, 84 | completion: completion) 85 | } 86 | } 87 | 88 | /// 89 | /// Pops viewControllers from the rootViewController's navigation stack until only one viewController 90 | /// is left. 91 | /// 92 | /// - Parameter animation: 93 | /// The animation to set for the presentable. Only its dismissalAnimation is used for the 94 | /// pop-transition. Specify `nil` here to leave animations as they were set for the 95 | /// presentable before. You can use `Animation.default` to reset the previously set animations 96 | /// on this presentable. 97 | /// 98 | public static func popToRoot(animation: Animation? = nil) -> Transition { 99 | Transition(presentables: [], 100 | animationInUse: animation?.dismissalAnimation 101 | ) { rootViewController, options, completion in 102 | rootViewController.pop(toRoot: true, 103 | with: options, 104 | animation: animation, 105 | completion: completion) 106 | } 107 | } 108 | 109 | /// 110 | /// Replaces the navigation stack of the rootViewController with the specified presentables. 111 | /// 112 | /// - Parameters: 113 | /// - presentables: The presentables to make up the navigation stack after the transition is done. 114 | /// - animation: 115 | /// The animation to set for the presentable. Its presentationAnimation will be used for the 116 | /// transition animation of the top-most viewController, its dismissalAnimation is used for 117 | /// any pop-transition of the whole navigation stack, if not otherwise specified. Specify `nil` 118 | /// here to leave animations as they were set for the presentables before. You can use 119 | /// `Animation.default` to reset the previously set animations on all presentables. 120 | /// 121 | public static func set(_ presentables: [Presentable], animation: Animation? = nil) -> Transition { 122 | Transition(presentables: presentables, 123 | animationInUse: animation?.presentationAnimation 124 | ) { rootViewController, options, completion in 125 | rootViewController.set(presentables.map { $0.viewController }, 126 | with: options, 127 | animation: animation 128 | ) { 129 | presentables.forEach { $0.presented(from: rootViewController) } 130 | completion?() 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/XCoordinator/PageCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageCoordinator.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 30.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// PageCoordinator provides a base class for your custom coordinator with a `UIPageViewController` rootViewController. 13 | /// 14 | /// - Note: 15 | /// PageCoordinator sets the dataSource of the rootViewController to reflect the parameters in the initializer. 16 | /// 17 | open class PageCoordinator: BaseCoordinator { 18 | 19 | // MARK: Stored properties 20 | 21 | /// 22 | /// The dataSource of the rootViewController. 23 | /// 24 | /// Feel free to change the pages at runtime. To reflect the changes in the rootViewController, perform a `set` transition as well. 25 | /// 26 | public let dataSource: UIPageViewControllerDataSource 27 | 28 | // MARK: Initialization 29 | 30 | /// 31 | /// Creates a PageCoordinator with several sequential (potentially looping) pages. 32 | /// 33 | /// It further sets the current page of the rootViewController animated in the specified direction. 34 | /// 35 | /// - Note: 36 | /// If you need custom configuration of the rootViewController, modify the `configuration` parameter, 37 | /// since you cannot change this after the initialization. 38 | /// 39 | /// - Parameters: 40 | /// - pages: 41 | /// The pages of the PageCoordinator. 42 | /// These can be changed later, if necessary, using the `PageCoordinator.dataSource` property. 43 | /// - loop: 44 | /// Whether or not the PageCoordinator should loop when hitting the end or the beginning of the specified pages. 45 | /// - set: 46 | /// The presentable to be shown right from the start. 47 | /// This should be one of the elements of the specified pages. 48 | /// If not specified, no `set` transition is triggered, which results in the first page being shown. 49 | /// - direction: 50 | /// The direction in which the transition to set the specified first page (parameter `set`) should be animated in. 51 | /// If you specify `nil` for `set`, this parameter is ignored. 52 | /// - configuration: 53 | /// The configuration of the rootViewController. You cannot change this configuration later anymore (Limitation of UIKit). 54 | /// 55 | public init(rootViewController: RootViewController = .init(), 56 | pages: [Presentable], 57 | loop: Bool = false, 58 | set: Presentable? = nil, 59 | direction: UIPageViewController.NavigationDirection = .forward) { 60 | self.dataSource = PageCoordinatorDataSource(pages: pages.map { $0.viewController }, loop: loop) 61 | rootViewController.dataSource = dataSource 62 | 63 | guard let firstPage = set ?? pages.first else { 64 | assertionFailure("Please provide a positive number of pages for use in \(String(describing: PageCoordinator.self))") 65 | super.init(rootViewController: rootViewController, initialTransition: .initial(pages: pages)) 66 | return 67 | } 68 | 69 | super.init(rootViewController: rootViewController, 70 | initialTransition: .multiple(.initial(pages: pages), .set(firstPage, direction: direction))) 71 | } 72 | 73 | /// 74 | /// Creates a PageCoordinator with a custom dataSource. 75 | /// It further sets the currently shown page and a direction for the animation of displaying it. 76 | /// If you need custom configuration of the rootViewController, modify the `configuration` parameter, 77 | /// since you cannot change this after the initialization. 78 | /// 79 | /// - Parameters: 80 | /// - dataSource: 81 | /// The dataSource of the PageCoordinator. 82 | /// - set: 83 | /// The presentable to be shown right from the start. 84 | /// This should be one of the elements of the specified pages. 85 | /// If not specified, no `set` transition is triggered, which results in the first page being shown. 86 | /// - direction: 87 | /// The direction in which the transition to set the specified first page (parameter `set`) should be animated in. 88 | /// If you specify `nil` for `set`, this parameter is ignored. 89 | /// - configuration: 90 | /// The configuration of the rootViewController. You cannot change this configuration later anymore (Limitation of UIKit). 91 | /// 92 | public init(rootViewController: RootViewController = .init(), 93 | dataSource: UIPageViewControllerDataSource, 94 | set: Presentable, 95 | direction: UIPageViewController.NavigationDirection) { 96 | self.dataSource = dataSource 97 | rootViewController.dataSource = dataSource 98 | super.init(rootViewController: rootViewController, 99 | initialTransition: .set(set, direction: direction)) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/XCoordinator/PageCoordinatorDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageCoordinatorDataSource.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 14.12.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// PageCoordinatorDataSource is a 13 | /// [UIPageViewControllerDataSource](https://developer.apple.com/documentation/uikit/UIPageViewControllerDataSource) 14 | /// implementation with a rather static list of pages. 15 | /// 16 | /// It further allows looping through the given pages. When looping is active the pages are wrapped around in the given presentables array. 17 | /// When the user navigates beyond the end of the specified pages, the pages are wrapped around by displaying the first page. 18 | /// In analogy to that, it also wraps to the last page when navigating beyond the beginning. 19 | /// 20 | open class PageCoordinatorDataSource: NSObject, UIPageViewControllerDataSource { 21 | 22 | // MARK: Stored properties 23 | 24 | /// The pages of the `UIPageViewController` in sequential order. 25 | open var pages: [UIViewController] 26 | 27 | /// Whether or not the pages of the `UIPageViewController` should be in a loop, 28 | /// i.e. whether a swipe to the left of the last page should result in the first page being shown 29 | /// (or the last shown when swiping right on the first page) 30 | open var loop: Bool 31 | 32 | // MARK: Initialization 33 | 34 | /// 35 | /// Creates a PageCoordinatorDataSource with the given pages and looping capabilities. 36 | /// 37 | /// - Parameters: 38 | /// - pages: 39 | /// The pages to be shown in the `UIPageViewController`. 40 | /// - loop: 41 | /// Whether or not the pages of the `UIPageViewController` should be in a loop, 42 | /// i.e. whether a swipe to the left of the last page should result in the first page being shown 43 | /// (or the last shown when swiping right on the first page) 44 | /// If you specify `false` here, the user cannot swipe left on the last page and right on the first. 45 | /// 46 | public init(pages: [UIViewController], loop: Bool) { 47 | self.pages = pages 48 | self.loop = loop 49 | } 50 | 51 | // MARK: Methods 52 | 53 | /// 54 | /// See [UIPageViewControllerDataSource](https://developer.apple.com/documentation/uikit/UIPageViewControllerDataSource) 55 | /// for further information. 56 | /// 57 | /// - Parameter pageViewController: 58 | /// The dataSource owner. 59 | /// 60 | /// - Returns: 61 | /// The count of `pages`, if it is displayed. Otherwise 0. 62 | /// 63 | open func presentationCount(for pageViewController: UIPageViewController) -> Int { 64 | let isNotDisplaying = pageViewController.viewControllers?.isEmpty ?? true 65 | return isNotDisplaying ? 0 : pages.count 66 | } 67 | 68 | /// 69 | /// See [UIPageViewControllerDataSource](https://developer.apple.com/documentation/uikit/UIPageViewControllerDataSource) 70 | /// for further information. 71 | /// 72 | /// - Parameter pageViewController: 73 | /// The dataSource owner. 74 | /// 75 | /// - Returns: 76 | /// The index of the currently visible view controller. 77 | /// 78 | open func presentationIndex(for pageViewController: UIPageViewController) -> Int { 79 | guard let viewController = pageViewController.viewControllers?.first else { return 0 } 80 | return pages.firstIndex(of: viewController) ?? 0 81 | } 82 | 83 | /// 84 | /// See [UIPageViewControllerDataSource](https://developer.apple.com/documentation/uikit/UIPageViewControllerDataSource) 85 | /// for further information. 86 | /// 87 | /// This method first searches for the index of the given viewController in the `pages` array. 88 | /// It then tries to find a viewController at the preceding position by potentially looping. 89 | /// 90 | /// - Parameters: 91 | /// - pageViewController: The dataSource owner. 92 | /// - viewController: The viewController to find the preceding viewController of. 93 | /// 94 | /// - Returns: 95 | /// The preceding viewController. 96 | /// 97 | open func pageViewController(_ pageViewController: UIPageViewController, 98 | viewControllerBefore viewController: UIViewController) -> UIViewController? { 99 | guard let index = pages.firstIndex(of: viewController) else { 100 | // swiftlint:disable:next line_length 101 | assertionFailure("\(String(describing: UIPageViewController.self)) is displaying viewController not available in the provided pages-array.") 102 | return nil 103 | } 104 | let prevIndex = index - 1 105 | guard prevIndex >= 0 else { return loop ? pages.last?.viewController : nil } 106 | return pages[prevIndex].viewController 107 | } 108 | 109 | /// 110 | /// See [UIPageViewControllerDataSource](https://developer.apple.com/documentation/uikit/UIPageViewControllerDataSource) 111 | /// for further information. 112 | /// 113 | /// This method first searches for the index of the given viewController in the `pages` array. 114 | /// It then tries to find a viewController at the following position by potentially looping. 115 | /// 116 | /// - Parameters: 117 | /// - pageViewController: The dataSource owner. 118 | /// - viewController: The viewController to find the following viewController of. 119 | /// 120 | /// - Returns: 121 | /// The following viewController. 122 | /// 123 | open func pageViewController(_ pageViewController: UIPageViewController, 124 | viewControllerAfter viewController: UIViewController) -> UIViewController? { 125 | guard let index = pages.firstIndex(of: viewController) else { 126 | // swiftlint:disable:next line_length 127 | assertionFailure("\(String(describing: UIPageViewController.self)) is displaying viewController not available in the provided pages-array.") 128 | return nil 129 | } 130 | let nextIndex = index + 1 131 | guard nextIndex < pages.count else { return loop ? pages.first?.viewController : nil } 132 | return pages[nextIndex].viewController 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/XCoordinator/PageTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageViewTransition.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 29.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// PageTransition offers transitions that can be used 12 | /// with a `UIPageViewController` rootViewController. 13 | public typealias PageTransition = Transition 14 | 15 | extension Transition where RootViewController: UIPageViewController { 16 | 17 | /// 18 | /// Sets the current page(s) of the rootViewController. Make sure to set 19 | /// `UIPageViewController.isDoubleSided` to the appropriate setting before executing this transition. 20 | /// 21 | /// - Parameters: 22 | /// - first: 23 | /// The first page being shown. If second is specified as `nil`, this reflects a single page 24 | /// being shown. 25 | /// - second: 26 | /// The second page being shown. This page is optional, as your rootViewController can be used 27 | /// with `isDoubleSided` enabled or not. 28 | /// - direction: 29 | /// The direction in which the transition should be animated. 30 | /// 31 | public static func set(_ first: Presentable, _ second: Presentable? = nil, 32 | direction: UIPageViewController.NavigationDirection) -> Transition { 33 | let presentables = [first, second].compactMap { $0 } 34 | return Transition(presentables: presentables, 35 | animationInUse: nil 36 | ) { rootViewController, options, completion in 37 | rootViewController.set(presentables.map { $0.viewController }, 38 | direction: direction, 39 | with: options 40 | ) { 41 | presentables.forEach { $0.presented(from: rootViewController) } 42 | completion?() 43 | } 44 | } 45 | } 46 | 47 | static func initial(pages: [Presentable]) -> Transition { 48 | Transition(presentables: pages, animationInUse: nil) { rootViewController, _, completion in 49 | CATransaction.begin() 50 | CATransaction.setCompletionBlock { 51 | pages.forEach { $0.presented(from: rootViewController) } 52 | completion?() 53 | } 54 | CATransaction.commit() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/XCoordinator/Presentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Presentable.swift 3 | // XCoordinator 4 | // 5 | // Created by Joan Disho on 03.05.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// Presentable represents all objects that can be presented (i.e. shown) to the user. 13 | /// 14 | /// Therefore, it is useful for view controllers, coordinators and views. 15 | /// Presentable is often used for transitions to allow for view controllers and coordinators to be transitioned to. 16 | /// 17 | public protocol Presentable { 18 | 19 | /// 20 | /// The viewController of the Presentable. 21 | /// 22 | /// In the case of a `UIViewController`, it returns itself. 23 | /// A coordinator returns its rootViewController. 24 | /// 25 | var viewController: UIViewController! { get } 26 | 27 | /// 28 | /// This method can be used to retrieve whether the presentable can trigger a specific route 29 | /// and potentially returns a router to trigger the route on. 30 | /// 31 | /// Deep linking makes use of this method to trigger the specified routes. 32 | /// 33 | /// - Parameter route: 34 | /// The route to determine a router for. 35 | /// 36 | func router(for route: R) -> StrongRouter? 37 | 38 | /// 39 | /// This method is called whenever a Presentable is shown to the user. 40 | /// It further provides information about the context a presentable is shown in. 41 | /// 42 | /// - Parameter presentable: 43 | /// The context in which the presentable is shown. 44 | /// This could be a window, another viewController, a coordinator, etc. 45 | /// `nil` is specified whenever a context cannot be easily determined. 46 | /// 47 | func presented(from presentable: Presentable?) 48 | 49 | /// 50 | /// This method is used to register a parent coordinator to a child coordinator. 51 | /// 52 | /// - Note: 53 | /// This method is used internally and should never be called directly. 54 | /// 55 | func registerParent(_ presentable: Presentable & AnyObject) 56 | 57 | /// 58 | /// This method gets called when the transition of a child coordinator is being reported to its parent. 59 | /// 60 | /// - Note: 61 | /// This method is used internally and should never be called directly. 62 | /// 63 | func childTransitionCompleted() 64 | 65 | /// 66 | /// Sets the presentable as the root of the window. 67 | /// 68 | /// This method sets the rootViewController of the window and makes it key and visible. 69 | /// Furthermore, it calls `presented(from:)` with the window as its parameter. 70 | /// 71 | /// - Parameter window: 72 | /// The window to set the root of. 73 | /// 74 | func setRoot(for window: UIWindow) 75 | } 76 | 77 | extension Presentable { 78 | 79 | public func registerParent(_ presentable: Presentable & AnyObject) {} 80 | 81 | public func childTransitionCompleted() {} 82 | 83 | public func setRoot(for window: UIWindow) { 84 | window.rootViewController = viewController 85 | window.makeKeyAndVisible() 86 | presented(from: window) 87 | } 88 | 89 | public func router(for route: R) -> StrongRouter? { 90 | self as? StrongRouter 91 | } 92 | 93 | public func presented(from presentable: Presentable?) {} 94 | } 95 | 96 | extension UIViewController: Presentable {} 97 | extension UIWindow: Presentable {} 98 | -------------------------------------------------------------------------------- /Sources/XCoordinator/RedirectionRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedirectionRouter.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 26.10.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// RedirectionRouters can be used to extract routes into different route types. 13 | /// Instead of having one huge route and one or more huge coordinators, you can create separate redirecting routers. 14 | /// 15 | /// Create a RedirectionRouter from a parent router by providing a reference to that parent. 16 | /// Triggered routes of the RedirectionRouter will be redirected to this parent router according to the provided mapping. 17 | /// Please provide either a `map` closure in the initializer or override the `mapToParentRoute` method. 18 | /// 19 | /// A RedirectionRouter has a viewController which is used in transitions, 20 | /// e.g. when you are presenting, pushing, or otherwise displaying it. 21 | /// 22 | open class RedirectionRouter: Router { 23 | 24 | // MARK: Stored properties 25 | 26 | /// A type-erased Router object of the parent router. 27 | public let parent: UnownedRouter 28 | 29 | private let _map: ((RouteType) -> ParentRoute)? 30 | 31 | // MARK: Computed properties 32 | 33 | /// 34 | /// The viewController used in transitions, e.g. when pushing, presenting 35 | /// or otherwise displaying the RedirectionRouter. 36 | /// 37 | public private(set) var viewController: UIViewController! 38 | 39 | // MARK: Initialization 40 | 41 | /// 42 | /// Creates a RedirectionRouter with a certain viewController, a parent router 43 | /// and an optional mapping. 44 | /// 45 | /// - Note: 46 | /// Make sure to either override `mapToSuperRoute` or to specify a closure for the `map` parameter. 47 | /// If you override `mapToSuperRoute`, the `map` parameter is ignored. 48 | /// 49 | /// - Parameters: 50 | /// - viewController: 51 | /// The view controller to be used in transitions, e.g. when pushing, presenting or otherwise displaying the RedirectionRouter. 52 | /// - parent: 53 | /// Triggered routes will be rerouted to the parent router. 54 | /// - map: 55 | /// A mapping from this RedirectionRouter's routes to the parent's routes. 56 | /// 57 | public init(viewController: UIViewController, 58 | parent: UnownedRouter, 59 | map: ((RouteType) -> ParentRoute)?) { 60 | self.parent = parent 61 | self._map = map 62 | self.viewController = viewController 63 | } 64 | 65 | // MARK: Methods 66 | 67 | open func contextTrigger(_ route: RouteType, 68 | with options: TransitionOptions, 69 | completion: ContextPresentationHandler?) { 70 | parent.contextTrigger(mapToParentRoute(route), with: options, completion: completion) 71 | } 72 | 73 | /// 74 | /// Map RouteType to ParentRoute. 75 | /// 76 | /// This method is called when a route is triggered in the RedirectionRouter. 77 | /// It is used to translate RouteType routes to the parent's routes which are then triggered in the parent router. 78 | /// 79 | /// - Parameter route: 80 | /// The route to be mapped. 81 | /// 82 | /// - Returns: 83 | /// The mapped route for the parent router. 84 | /// 85 | open func mapToParentRoute(_ route: RouteType) -> ParentRoute { 86 | guard let map = self._map else { 87 | fatalError("Please implement \(#function) or use the `map` closure in the initializer.") 88 | } 89 | return map(route) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/XCoordinator/Route.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Route.swift 3 | // XCoordinator 4 | // 5 | // Created by Stefan Kofler on 30.04.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// This is the protocol your route types need to conform to. 13 | /// 14 | /// - Note: 15 | /// It has no requirements, although the use of enums is encouraged to make your 16 | /// navigation code type safe. 17 | /// 18 | public protocol Route {} 19 | -------------------------------------------------------------------------------- /Sources/XCoordinator/Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteTrigger.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 28.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 12 | /// The Router protocol is used to abstract the transition-type specific characteristics of a Coordinator. 13 | /// 14 | /// A Router can trigger routes, which lead to transitions being executed. In constrast to the Coordinator protocol, 15 | /// the router does not specify a TransitionType and can therefore be used in the form of a 16 | /// `StrongRouter`, `UnownedRouter` or `WeakRouter` to reduce a coordinator's capabilities to 17 | /// the triggering of routes. 18 | /// This may especially be useful in viewModels when using them in different contexts. 19 | /// 20 | public protocol Router: Presentable { 21 | 22 | /// RouteType defines which routes can be triggered in a certain Router implementation. 23 | associatedtype RouteType: Route 24 | 25 | /// 26 | /// Triggers routes and returns context in completion-handler. 27 | /// 28 | /// Useful for deep linking. It is encouraged to use `trigger` instead, if the context is not needed. 29 | /// 30 | /// - Parameters: 31 | /// - route: The route to be triggered. 32 | /// - options: 33 | /// Transition options configuring the execution of transitions, e.g. whether it should be animated. 34 | /// - completion: 35 | /// If present, this completion handler is executed once the transition is completed 36 | /// (including animations). 37 | /// If the context is not needed, use `trigger` instead. 38 | /// 39 | func contextTrigger(_ route: RouteType, with options: TransitionOptions, completion: ContextPresentationHandler?) 40 | } 41 | 42 | extension Router { 43 | 44 | // MARK: Convenience methods 45 | 46 | /// 47 | /// Triggers the specified route without the need of specifying a completion handler. 48 | /// 49 | /// - Parameters: 50 | /// - route: The route to be triggered. 51 | /// - options: 52 | /// Transition options for performing the transition, e.g. whether it should be animated. 53 | /// 54 | public func trigger(_ route: RouteType, with options: TransitionOptions) { 55 | trigger(route, with: options, completion: nil) 56 | } 57 | 58 | /// 59 | /// Triggers the specified route with default transition options enabling the animation of the transition. 60 | /// 61 | /// - Parameters: 62 | /// - route: The route to be triggered. 63 | /// - completion: 64 | /// If present, this completion handler is executed once the transition is completed 65 | /// (including animations). 66 | /// 67 | public func trigger(_ route: RouteType, completion: PresentationHandler? = nil) { 68 | trigger(route, with: .default, completion: completion) 69 | } 70 | 71 | /// 72 | /// Triggers the specified route by performing a transition. 73 | /// 74 | /// - Parameters: 75 | /// - route: The route to be triggered. 76 | /// - options: Transition options for performing the transition, e.g. whether it should be animated. 77 | /// - completion: 78 | /// If present, this completion handler is executed once the transition is completed 79 | /// (including animations). 80 | /// 81 | public func trigger(_ route: RouteType, with options: TransitionOptions, completion: PresentationHandler?) { 82 | autoreleasepool { 83 | contextTrigger(route, with: options) { _ in completion?() } 84 | } 85 | } 86 | 87 | } 88 | 89 | extension Router { 90 | 91 | // MARK: Computed properties 92 | 93 | /// 94 | /// Creates a StrongRouter object from the given router to abstract from concrete implementations 95 | /// while maintaining information necessary to fulfill the Router protocol. 96 | /// The original router will be held strongly. 97 | /// 98 | public var strongRouter: StrongRouter { 99 | StrongRouter(self) 100 | } 101 | 102 | /// 103 | /// Returns a router for the specified route, if possible. 104 | /// 105 | /// - Parameter route: 106 | /// The route type to return a router for. 107 | /// 108 | /// - Returns: 109 | /// It returns the router's strongRouter, 110 | /// if it is compatible with the given route type, 111 | /// otherwise `nil`. 112 | /// 113 | public func router(for route: R) -> StrongRouter? { 114 | strongRouter as? StrongRouter 115 | } 116 | 117 | } 118 | 119 | #if swift(>=5.5.2) 120 | 121 | @available(iOS 13.0, tvOS 13.0, *) 122 | extension Router { 123 | 124 | /// 125 | /// Triggers the specified route with default transition options enabling the animation of the transition. 126 | /// 127 | /// - Parameters: 128 | /// - route: The route to be triggered. 129 | /// 130 | @MainActor public func trigger(_ route: RouteType) async { 131 | await trigger(route, with: .default) 132 | } 133 | 134 | /// 135 | /// Triggers the specified route by performing a transition. 136 | /// 137 | /// - Parameters: 138 | /// - route: The route to be triggered. 139 | /// - options: Transition options for performing the transition, e.g. whether it should be animated. 140 | /// 141 | @MainActor public func trigger(_ route: RouteType, with options: TransitionOptions) async { 142 | _ = await contextTrigger(route, with: options) 143 | } 144 | 145 | /// 146 | /// Triggers routes and returns context in completion-handler. 147 | /// 148 | /// Useful for deep linking. It is encouraged to use `trigger` instead, if the context is not needed. 149 | /// 150 | /// - Parameters: 151 | /// - route: The route to be triggered. 152 | /// - options: 153 | /// Transition options configuring the execution of transitions, e.g. whether it should be animated. 154 | /// - completion: 155 | /// If present, this completion handler is executed once the transition is completed 156 | /// (including animations). 157 | /// 158 | /// - Returns: 159 | /// The transition context of the performed transition(s). 160 | /// If the context is not needed, use `trigger` instead. 161 | /// 162 | @MainActor public func contextTrigger(_ route: RouteType, with options: TransitionOptions) async -> TransitionContext { 163 | await withCheckedContinuation { continuation in 164 | contextTrigger(route, with: options) { context in 165 | continuation.resume(returning: context) 166 | } 167 | } 168 | } 169 | 170 | } 171 | 172 | #endif 173 | -------------------------------------------------------------------------------- /Sources/XCoordinator/SplitCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitCoordinator.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 30.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | /// 10 | /// SplitCoordinator can be used as a basis for a coordinator with a rootViewController of type 11 | /// `UISplitViewController`. 12 | /// 13 | /// You can use all `SplitTransitions` and get an initializer to set a master and 14 | /// (optional) detail presentable. 15 | /// 16 | open class SplitCoordinator: BaseCoordinator { 17 | 18 | // MARK: Initialization 19 | 20 | public override init(rootViewController: RootViewController = .init(), initialRoute: RouteType?) { 21 | super.init(rootViewController: rootViewController, initialRoute: initialRoute) 22 | } 23 | 24 | /// 25 | /// Creates a SplitCoordinator and sets the specified presentables as the rootViewController's 26 | /// viewControllers. 27 | /// 28 | /// - Parameters: 29 | /// - master: 30 | /// The presentable to be shown as master in the `UISplitViewController`. 31 | /// - detail: 32 | /// The presentable to be shown as detail in the `UISplitViewController`. This is optional due to 33 | /// the fact that it might not be useful to have a detail page right away on a small-screen device. 34 | /// 35 | public init(rootViewController: RootViewController = .init(), master: Presentable, detail: Presentable?) { 36 | super.init(rootViewController: rootViewController, 37 | initialTransition: .set([master, detail].compactMap { $0 })) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/XCoordinator/SplitTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UISplitViewController+Transition.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 10.01.19. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// SplitTransition offers different transitions common to a `UISplitViewController` rootViewController. 13 | /// 14 | public typealias SplitTransition = Transition 15 | 16 | extension Transition where RootViewController: UISplitViewController { 17 | 18 | public static func set(_ presentables: [Presentable]) -> Transition { 19 | Transition(presentables: presentables, animationInUse: nil) { rootViewController, _, completion in 20 | CATransaction.begin() 21 | CATransaction.setCompletionBlock { 22 | presentables.forEach { $0.presented(from: rootViewController) } 23 | completion?() 24 | } 25 | autoreleasepool { 26 | rootViewController.viewControllers = presentables.map { $0.viewController } 27 | } 28 | CATransaction.commit() 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/XCoordinator/StaticTransitionAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaticTransitionAnimation.swift 3 | // XCoordinator 4 | // 5 | // Created by Stefan Kofler on 03.05.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// `StaticTransitionAnimation` can be used to realize static transition animations. 13 | /// 14 | /// - Note: 15 | /// Consider using `InteractiveTransitionAnimation` instead, if possible, as it is as simple 16 | /// to use. However, this class is helpful to make sure your transition animation is not mistaken to be 17 | /// interactive, if your animation code does not fulfill the requirements of an interactive transition 18 | /// animation. 19 | /// 20 | open class StaticTransitionAnimation: NSObject, TransitionAnimation { 21 | 22 | // MARK: Stored properties 23 | 24 | internal let duration: TimeInterval 25 | private let _performAnimation: (_ transitionContext: UIViewControllerContextTransitioning) -> Void 26 | 27 | // MARK: Computed properties 28 | 29 | open var interactionController: PercentDrivenInteractionController? { 30 | self as? PercentDrivenInteractionController 31 | } 32 | 33 | // MARK: Initialization 34 | 35 | /// 36 | /// Creates a StaticTransitionAnimation to be used as presentation or dismissal transition animation in 37 | /// an `Animation` object. 38 | /// 39 | /// - Parameters: 40 | /// - duration: The total duration of the animation. 41 | /// - performAnimation: A closure performing the animation. 42 | /// - context: 43 | /// From the context, you can access source and destination views and 44 | /// viewControllers and the containerView. 45 | /// 46 | public init(duration: TimeInterval, performAnimation: @escaping (_ context: UIViewControllerContextTransitioning) -> Void) { 47 | self.duration = duration 48 | self._performAnimation = performAnimation 49 | } 50 | 51 | // MARK: Methods 52 | 53 | /// 54 | /// See [UIViewControllerAnimatedTransitioning](https://developer.apple.com/documentation/uikit/UIViewControllerAnimatedTransitioning) 55 | /// for further information. 56 | /// 57 | /// - Parameter transitionContext: 58 | /// The context of the current transition. 59 | /// 60 | /// - Returns: 61 | /// The duration of the animation as specified in the initializer. 62 | /// 63 | open func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 64 | duration 65 | } 66 | 67 | /// 68 | /// See [UIViewControllerAnimatedTransitioning](https://developer.apple.com/documentation/uikit/UIViewControllerAnimatedTransitioning) 69 | /// for further information. 70 | /// 71 | /// This method performs the animation as specified in the initializer. 72 | /// 73 | /// - Parameter transitionContext: 74 | /// The context of the current transition. 75 | /// 76 | open func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 77 | _performAnimation(transitionContext) 78 | } 79 | 80 | // MARK: TransitionAnimation 81 | 82 | open func start() {} 83 | open func cleanup() {} 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Sources/XCoordinator/StrongRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StrongRouter.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 28.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// StrongRouter is a type-erasure of a given Router object and, therefore, can be used as an abstraction from a specific Router 13 | /// implementation without losing type information about its RouteType. 14 | /// 15 | /// StrongRouter abstracts away any implementation specific details and 16 | /// essentially reduces them to properties specified in the `Router` protocol. 17 | /// 18 | /// - Note: 19 | /// Do not hold a reference to any router from the view hierarchy. 20 | /// Use `UnownedRouter` or `WeakRouter` in your view controllers or view models instead. 21 | /// You can create them using the `Coordinator.unownedRouter` and `Coordinator.weakRouter` properties. 22 | /// 23 | public final class StrongRouter: Router { 24 | 25 | // MARK: Stored properties 26 | 27 | private let _contextTrigger: (RouteType, TransitionOptions, ContextPresentationHandler?) -> Void 28 | private let _trigger: (RouteType, TransitionOptions, PresentationHandler?) -> Void 29 | private let _presented: (Presentable?) -> Void 30 | private let _viewController: () -> UIViewController? 31 | private let _setRoot: (UIWindow) -> Void 32 | private let _registerParent: (Presentable & AnyObject) -> Void 33 | private let _childTransitionCompleted: () -> Void 34 | 35 | // MARK: Initialization 36 | 37 | /// 38 | /// Creates a StrongRouter object from a given router. 39 | /// 40 | /// - Parameter router: 41 | /// The source router. 42 | /// 43 | public init(_ router: T) where T.RouteType == RouteType { 44 | _trigger = router.trigger 45 | _presented = router.presented 46 | _viewController = { router.viewController } 47 | _setRoot = router.setRoot 48 | _contextTrigger = router.contextTrigger 49 | _registerParent = router.registerParent 50 | _childTransitionCompleted = router.childTransitionCompleted 51 | } 52 | 53 | // MARK: Public methods 54 | 55 | /// 56 | /// Triggers routes and provides the transition context in the completion-handler. 57 | /// 58 | /// Useful for deep linking. It is encouraged to use `trigger` instead, if the context is not needed. 59 | /// 60 | /// - Parameters: 61 | /// - route: The route to be triggered. 62 | /// - options: Transition options configuring the execution of transitions, e.g. whether it should be animated. 63 | /// - completion: 64 | /// If present, this completion handler is executed once the transition is completed 65 | /// (including animations). 66 | /// If the context is not needed, use `trigger` instead. 67 | /// 68 | public func contextTrigger(_ route: RouteType, 69 | with options: TransitionOptions, 70 | completion: ContextPresentationHandler?) { 71 | _contextTrigger(route, options, completion) 72 | } 73 | 74 | /// 75 | /// Triggers the specified route by performing a transition. 76 | /// 77 | /// - Parameters: 78 | /// - route: The route to be triggered. 79 | /// - options: Transition options for performing the transition, e.g. whether it should be animated. 80 | /// - completion: 81 | /// If present, this completion handler is executed once the transition is completed 82 | /// (including animations). 83 | /// 84 | public func trigger(_ route: RouteType, with options: TransitionOptions, completion: PresentationHandler?) { 85 | _trigger(route, options, completion) 86 | } 87 | 88 | /// 89 | /// This method is called whenever a Presentable is shown to the user. 90 | /// It further provides information about the presentable responsible for the presenting. 91 | /// 92 | /// - Parameter presentable: 93 | /// The context in which the presentable is shown. 94 | /// This could be a window, another viewController, a coordinator, etc. 95 | /// `nil` is specified whenever a context cannot be easily determined. 96 | /// 97 | public func presented(from presentable: Presentable?) { 98 | _presented(presentable) 99 | } 100 | 101 | /// 102 | /// The viewController of the Presentable. 103 | /// 104 | /// In the case of a `UIViewController`, it returns itself. 105 | /// A coordinator returns its rootViewController. 106 | /// 107 | public var viewController: UIViewController! { 108 | _viewController() 109 | } 110 | 111 | public func registerParent(_ presentable: Presentable & AnyObject) { 112 | _registerParent(presentable) 113 | } 114 | 115 | public func childTransitionCompleted() { 116 | _childTransitionCompleted() 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /Sources/XCoordinator/TabBarAnimationDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarAnimationDelegate.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 24.10.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// TabBarAnimationDelegate is used as the delegate of a TabBarCoordinator's rootViewController 13 | /// to allow for transitions to specify transition animations. 14 | /// 15 | /// TabBarAnimationDelegate conforms to the `UITabBarControllerDelegate` protocol 16 | /// and is intended for use as the delegate of one tabbar controller only. 17 | /// 18 | /// - Note: 19 | /// Do not override the delegate of a TabBarCoordinator's rootViewController-delegate. 20 | /// Instead use the delegate property of the TabBarCoordinator itself. 21 | /// 22 | open class TabBarAnimationDelegate: NSObject { 23 | 24 | // MARK: Stored properties 25 | 26 | internal weak var delegate: UITabBarControllerDelegate? 27 | } 28 | 29 | // MARK: - UITabBarControllerDelegate 30 | 31 | extension TabBarAnimationDelegate: UITabBarControllerDelegate { 32 | 33 | /// 34 | /// See [UITabBarControllerDelegate](https://developer.apple.com/documentation/uikit/UITabBarControllerDelegate) 35 | /// for further reference. 36 | /// 37 | /// - Parameters 38 | /// - tabBarController: The delegate owner. 39 | /// - animationController: The animationController to return the interactionController for. 40 | /// 41 | /// - Returns: 42 | /// If the animationController is a `TransitionAnimation`, it returns its interactionController. 43 | /// Otherwise it requests an interactionController from the TabBarCoordinator's delegate. 44 | /// 45 | open func tabBarController(_ tabBarController: UITabBarController, 46 | interactionControllerFor animationController: UIViewControllerAnimatedTransitioning 47 | ) -> UIViewControllerInteractiveTransitioning? { 48 | (animationController as? TransitionAnimation)?.interactionController 49 | ?? delegate?.tabBarController?(tabBarController, interactionControllerFor: animationController) 50 | } 51 | 52 | /// 53 | /// See [UITabBarControllerDelegate](https://developer.apple.com/documentation/uikit/UITabBarControllerDelegate) 54 | /// for further reference. 55 | /// 56 | /// - Parameters: 57 | /// - tabBarController: The delegate owner. 58 | /// - fromVC: The source view controller of the transition. 59 | /// - toVC: The destination view controller of the transition. 60 | /// 61 | /// - Returns: 62 | /// The presentation animation controller from the toVC's transitioningDelegate. 63 | /// If not present, it uses the TabBarCoordinator's delegate as fallback. 64 | /// 65 | open func tabBarController(_ tabBarController: UITabBarController, 66 | animationControllerForTransitionFrom fromVC: UIViewController, 67 | to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { 68 | toVC.transitioningDelegate?.animationController?(forPresented: toVC, presenting: tabBarController, source: fromVC) 69 | ?? delegate?.tabBarController?(tabBarController, animationControllerForTransitionFrom: fromVC, to: toVC) 70 | } 71 | 72 | /// 73 | /// See [UITabBarControllerDelegate](https://developer.apple.com/documentation/uikit/UITabBarControllerDelegate) 74 | /// for further reference. 75 | /// 76 | /// This method delegates to the TabBarCoordinator's delegate. 77 | /// 78 | /// - Parameters: 79 | /// - tabBarController: The delegate owner. 80 | /// - viewController: The destination viewController. 81 | /// 82 | open func tabBarController(_ tabBarController: UITabBarController, 83 | didSelect viewController: UIViewController) { 84 | delegate?.tabBarController?(tabBarController, didSelect: viewController) 85 | } 86 | 87 | /// 88 | /// See [UITabBarControllerDelegate](https://developer.apple.com/documentation/uikit/UITabBarControllerDelegate) 89 | /// for further reference. 90 | /// 91 | /// This method delegates to the TabBarCoordinator's delegate. 92 | /// 93 | /// - Parameters: 94 | /// - tabBarController: The delegate owner. 95 | /// - viewController: The destination viewController. 96 | /// 97 | /// - Returns: 98 | /// The result of the TabBarCooordinator's delegate. If not specified, it returns true. 99 | /// 100 | open func tabBarController(_ tabBarController: UITabBarController, 101 | shouldSelect viewController: UIViewController) -> Bool { 102 | delegate?.tabBarController?(tabBarController, shouldSelect: viewController) ?? true 103 | } 104 | 105 | #if !os(tvOS) 106 | 107 | /// 108 | /// See [UITabBarControllerDelegate](https://developer.apple.com/documentation/uikit/UITabBarControllerDelegate) 109 | /// for further reference. 110 | /// 111 | /// This method delegates to the TabBarCoordinator's delegate. 112 | /// 113 | /// - Parameters: 114 | /// - tabBarController: The delegate owner. 115 | /// - viewControllers: The source viewControllers. 116 | /// 117 | open func tabBarController(_ tabBarController: UITabBarController, 118 | willBeginCustomizing viewControllers: [UIViewController]) { 119 | delegate?.tabBarController?(tabBarController, willBeginCustomizing: viewControllers) 120 | } 121 | 122 | /// 123 | /// See [UITabBarControllerDelegate](https://developer.apple.com/documentation/uikit/UITabBarControllerDelegate) 124 | /// for further reference. 125 | /// 126 | /// This method delegates to the TabBarCoordinator's delegate. 127 | /// 128 | /// - Parameters: 129 | /// - tabBarController: The delegate owner. 130 | /// - viewControllers: The source viewControllers. 131 | /// 132 | open func tabBarController(_ tabBarController: UITabBarController, 133 | didEndCustomizing viewControllers: [UIViewController], changed: Bool) { 134 | delegate?.tabBarController?(tabBarController, didEndCustomizing: viewControllers, changed: changed) 135 | } 136 | 137 | /// 138 | /// See [UITabBarControllerDelegate](https://developer.apple.com/documentation/uikit/UITabBarControllerDelegate) 139 | /// for further reference. 140 | /// 141 | /// This method delegates to the TabBarCoordinator's delegate. 142 | /// 143 | /// - Parameters: 144 | /// - tabBarController: The delegate owner. 145 | /// - viewControllers: The source viewControllers. 146 | /// 147 | open func tabBarController(_ tabBarController: UITabBarController, 148 | willEndCustomizing viewControllers: [UIViewController], changed: Bool) { 149 | delegate?.tabBarController?(tabBarController, willEndCustomizing: viewControllers, changed: changed) 150 | } 151 | 152 | #endif 153 | 154 | } 155 | 156 | extension UITabBarController { 157 | internal var animationDelegate: TabBarAnimationDelegate? { 158 | delegate as? TabBarAnimationDelegate 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/XCoordinator/TabBarCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarCoordinator.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 29.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// Use a TabBarCoordinator to coordinate a flow where a `UITabbarController` serves as a rootViewController. 13 | /// With a TabBarCoordinator, you get access to all tabbarController-related transitions. 14 | /// 15 | open class TabBarCoordinator: BaseCoordinator { 16 | 17 | // MARK: Stored properties 18 | 19 | /// 20 | /// The animation delegate controlling the rootViewController's transition animations. 21 | /// This animation delegate is set to be the rootViewController's rootViewController, if you did not set one earlier. 22 | /// 23 | /// - Note: 24 | /// Use the `delegate` property to set a custom delegate and use transition animations provided by XCoordinator. 25 | /// 26 | private let animationDelegate = TabBarAnimationDelegate() 27 | // swiftlint:disable:previous weak_delegate 28 | 29 | // MARK: Computed properties 30 | 31 | /// 32 | /// Use this delegate to get informed about tabbarController-related notifications and delegate methods 33 | /// specifying transition animations. The delegate is only referenced weakly. 34 | /// 35 | /// Set this delegate instead of overriding the delegate of the rootViewController 36 | /// specified in the initializer, if possible, to allow for transition animations 37 | /// to be executed as specified in the `prepareTransition(for:)` method. 38 | /// 39 | public var delegate: UITabBarControllerDelegate? { 40 | get { 41 | animationDelegate.delegate 42 | } 43 | set { 44 | animationDelegate.delegate = newValue 45 | } 46 | } 47 | 48 | // MARK: Initialization 49 | 50 | public override init(rootViewController: RootViewController = .init(), initialRoute: RouteType?) { 51 | if rootViewController.delegate == nil { 52 | rootViewController.delegate = animationDelegate 53 | } 54 | super.init(rootViewController: rootViewController, initialRoute: initialRoute) 55 | } 56 | 57 | /// 58 | /// Creates a TabBarCoordinator with a specified set of tabs. 59 | /// 60 | /// - Parameter tabs: 61 | /// The presentables to be used as tabs. 62 | /// 63 | public init(rootViewController: RootViewController = .init(), tabs: [Presentable]) { 64 | if rootViewController.delegate == nil { 65 | rootViewController.delegate = animationDelegate 66 | } 67 | super.init(rootViewController: rootViewController, initialTransition: .set(tabs)) 68 | } 69 | 70 | /// 71 | /// Creates a TabBarCoordinator with a specified set of tabs and selects a specific presentable. 72 | /// 73 | /// - Parameters: 74 | /// - tabs: The presentables to be used as tabs. 75 | /// - select: 76 | /// The presentable to be selected before displaying. Make sure, this presentable is one of the 77 | /// specified tabs in the other parameter. 78 | /// 79 | public init(rootViewController: RootViewController = .init(), tabs: [Presentable], select: Presentable) { 80 | if rootViewController.delegate == nil { 81 | rootViewController.delegate = animationDelegate 82 | } 83 | super.init(rootViewController: rootViewController, 84 | initialTransition: .multiple(.set(tabs), .select(select))) 85 | } 86 | 87 | /// 88 | /// Creates a TabBarCoordinator with a specified set of tabs and selects a presentable at a given index. 89 | /// 90 | /// - Parameters: 91 | /// - tabs: The presentables to be used as tabs. 92 | /// - select: The index of the presentable to be selected before displaying. 93 | /// 94 | public init(rootViewController: RootViewController = .init(), tabs: [Presentable], select: Int) { 95 | if rootViewController.delegate == nil { 96 | rootViewController.delegate = animationDelegate 97 | } 98 | super.init(rootViewController: rootViewController, 99 | initialTransition: .multiple(.set(tabs), .select(index: select))) 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /Sources/XCoordinator/TabBarTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarTransition.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 27.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// TabBarTransition offers transitions that can be used 12 | /// with a `UITabBarController` rootViewController. 13 | public typealias TabBarTransition = Transition 14 | 15 | extension Transition where RootViewController: UITabBarController { 16 | 17 | /// 18 | /// Transition to set the tabs of the rootViewController with an optional custom animation. 19 | /// 20 | /// - Note: 21 | /// Only the presentation animation of the Animation object is used. 22 | /// 23 | /// - Parameters: 24 | /// - presentables: 25 | /// The tabs to be set are defined by the presentables' viewControllers. 26 | /// - animation: 27 | /// The animation to be used. If you specify `nil` here, the default animation by UIKit is used. 28 | /// 29 | public static func set(_ presentables: [Presentable], animation: Animation? = nil) -> Transition { 30 | Transition(presentables: presentables, 31 | animationInUse: animation?.presentationAnimation 32 | ) { rootViewController, options, completion in 33 | rootViewController.set(presentables.map { $0.viewController }, 34 | with: options, 35 | animation: animation, 36 | completion: { 37 | presentables.forEach { $0.presented(from: rootViewController) } 38 | completion?() 39 | }) 40 | } 41 | } 42 | 43 | /// 44 | /// Transition to select a tab with an optional custom animation. 45 | /// 46 | /// - Note: 47 | /// Only the presentation animation of the Animation object is used. 48 | /// 49 | /// - Parameters: 50 | /// - presentable: 51 | /// The tab to be selected is the presentable's viewController. Make sure that this is one of the 52 | /// previously specified tabs of the rootViewController. 53 | /// - animation: 54 | /// The animation to be used. If you specify `nil` here, the default animation by UIKit is used. 55 | /// 56 | public static func select(_ presentable: Presentable, animation: Animation? = nil) -> Transition { 57 | Transition(presentables: [presentable], 58 | animationInUse: animation?.presentationAnimation 59 | ) { rootViewController, options, completion in 60 | rootViewController.select(presentable.viewController, 61 | with: options, 62 | animation: animation, 63 | completion: completion) 64 | } 65 | } 66 | 67 | /// 68 | /// Transition to select a tab with an optional custom animation. 69 | /// 70 | /// - Note: 71 | /// Only the presentation animation of the Animation object is used. 72 | /// 73 | /// - Parameters: 74 | /// - index: 75 | /// The index of the tab to be selected. Make sure that there is a tab at the specified index. 76 | /// - animation: 77 | /// The animation to be used. If you specify `nil` here, the default animation by UIKit is used. 78 | /// 79 | public static func select(index: Int, animation: Animation? = nil) -> Transition { 80 | Transition(presentables: [], 81 | animationInUse: animation?.presentationAnimation 82 | ) { rootViewController, options, completion in 83 | rootViewController.select(index: index, 84 | with: options, 85 | animation: animation, 86 | completion: completion) 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Sources/XCoordinator/Transition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Transition.swift 3 | // XCoordinator 4 | // 5 | // Created by Stefan Kofler on 30.04.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// This struct represents the common implementation of the `TransitionProtocol`. 13 | /// It is used in every of the provided `BaseCoordinator` subclasses and provides all transitions implemented in XCoordinator. 14 | /// 15 | /// `Transitions` are defined by a `Transition.Perform` closure. 16 | /// It further provides different context information such as `Transition.presentable` and `Transition.animation`. 17 | /// You can create your own custom transitions using `Transition.init(presentable:animation:perform:)` or 18 | /// use one of the many provided static functions to create the most common transitions. 19 | /// 20 | /// - Note: 21 | /// Transitions have a generic constraint to the rootViewController in use. 22 | /// Therefore, not all transitions are available in every coordinator. 23 | /// Make sure to specify the `RootViewController` type of the `TransitionType` of your coordinator as precise as possible 24 | /// to get all already available transitions. 25 | /// 26 | public struct Transition: TransitionProtocol { 27 | 28 | // MARK: Typealias 29 | 30 | /// 31 | /// Perform is the type of closure used to perform the transition. 32 | /// 33 | /// - Parameters: 34 | /// - rootViewController: 35 | /// The rootViewController to perform the transition on. 36 | /// - options: 37 | /// The options on how to perform the transition, e.g. whether it should be animated or not. 38 | /// - completion: 39 | /// The completion handler of the transition. 40 | /// It is called when the transition (including all animations) is completed. 41 | /// 42 | public typealias PerformClosure = (_ rootViewController: RootViewController, 43 | _ options: TransitionOptions, 44 | _ completion: PresentationHandler?) -> Void 45 | 46 | // MARK: Stored properties 47 | 48 | private var _presentables: [Presentable] 49 | private var _animation: TransitionAnimation? 50 | private var _perform: PerformClosure 51 | 52 | // MARK: Computed properties 53 | 54 | /// 55 | /// The presentables this transition is putting into the view hierarchy. This is especially useful for 56 | /// deep-linking. 57 | /// 58 | public var presentables: [Presentable] { 59 | _presentables 60 | } 61 | 62 | /// 63 | /// The transition animation this transition is using, i.e. the presentation or dismissal animation 64 | /// of the specified `Animation` object. If the transition does not use any transition animations, `nil` 65 | /// is returned. 66 | /// 67 | public var animation: TransitionAnimation? { 68 | _animation 69 | } 70 | 71 | // MARK: Initialization 72 | 73 | /// 74 | /// Create your custom transitions with this initializer. 75 | /// 76 | /// Extending Transition with static functions to create transitions with this initializer 77 | /// (instead of calling this initializer in your `prepareTransition(for:)` method) is advised 78 | /// as it makes reuse easier. 79 | /// 80 | /// - Parameters: 81 | /// - presentables: 82 | /// The presentables this transition is putting into the view hierarchy, if specifiable. 83 | /// These presentables are used in the deep-linking feature. 84 | /// - animationInUse: 85 | /// The transition animation this transition is using during the transition, i.e. the present animation 86 | /// of a presenting transition or the dismissal animation of a dismissing transition. 87 | /// Make sure to specify an animation here to use your transition with the 88 | /// `registerInteractiveTransition` method in your coordinator. 89 | /// - perform: 90 | /// The perform closure executes the transition. 91 | /// To create custom transitions, make sure to call the completion handler after all animations are done. 92 | /// If applicable, make sure to use the TransitionOptions to, e.g., decide whether a transition should be animated or not. 93 | /// 94 | public init(presentables: [Presentable], animationInUse: TransitionAnimation?, perform: @escaping PerformClosure) { 95 | self._presentables = presentables 96 | self._animation = animationInUse 97 | self._perform = perform 98 | } 99 | 100 | // MARK: Methods 101 | 102 | /// 103 | /// Performs a transition on the given viewController. 104 | /// 105 | /// - Warning: 106 | /// Do not call this method directly. Instead use your coordinator's `performTransition` method or trigger 107 | /// a specified route (latter option is encouraged). 108 | /// 109 | public func perform(on rootViewController: RootViewController, with options: TransitionOptions, completion: PresentationHandler?) { 110 | autoreleasepool { 111 | _perform(rootViewController, options, completion) 112 | } 113 | } 114 | 115 | } 116 | 117 | -------------------------------------------------------------------------------- /Sources/XCoordinator/TransitionAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionAnimation.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 26.11.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// TransitionAnimation aims to provide a common protocol for any type of transition animation used in an `Animation` object. 13 | /// 14 | /// XCoordinator provides different implementations of this protocol with the `StaticTransitionAnimation`, 15 | /// `InteractiveTransitionAnimation` and `InterruptibleTransitionAnimation` classes. 16 | /// 17 | public protocol TransitionAnimation: UIViewControllerAnimatedTransitioning { 18 | 19 | /// 20 | /// The interaction controller of an animation. 21 | /// It gets notified about the state of an animation and handles the specific events accordingly. 22 | /// 23 | /// The interaction controller is reset when calling `TransitionAnimation.start()` can always be `nil`, 24 | /// e.g. in static transition animations. 25 | /// 26 | /// Until `TransitionAnimation.cleanup()` is called, it should always return the same instance. 27 | /// 28 | var interactionController: PercentDrivenInteractionController? { get } 29 | 30 | /// Starts the animation by possibly creating a new interaction controller. 31 | func start() 32 | 33 | /// Cleans up a TransitionAnimation after an animation has been completed, e.g. by deleting an interaction controller. 34 | func cleanup() 35 | 36 | } 37 | 38 | /// 39 | /// PercentDrivenInteractionController is used for interaction controller types that can updated based on a percentage of completion. 40 | /// Furthermore, a PercentDrivenInteractionController should be able to cancel and finish a transition animation. 41 | /// 42 | /// PercentDrivenInteractionController is based on the `UIViewControllerInteractiveTransitioning` protocol. 43 | /// 44 | /// - Note: 45 | /// While you can implement your custom implementation, 46 | /// UIKit offers a default implementation with `UIPercentDrivenInteractiveTransition`. 47 | /// 48 | public protocol PercentDrivenInteractionController: UIViewControllerInteractiveTransitioning { 49 | 50 | /// 51 | /// Updates the animation to be at the specified progress. 52 | /// 53 | /// This method is called based on user interactions. 54 | /// A linear progression of the animation is encouraged when handling user interactions. 55 | /// 56 | func update(_ percentComplete: CGFloat) 57 | 58 | /// 59 | /// Cancels the animation, e.g. by cleaning up and reversing any progress made. 60 | /// 61 | func cancel() 62 | 63 | /// 64 | /// Finishes the animation by completing it from the current progress onwards. 65 | /// 66 | func finish() 67 | 68 | } 69 | 70 | extension UIPercentDrivenInteractiveTransition: PercentDrivenInteractionController {} 71 | -------------------------------------------------------------------------------- /Sources/XCoordinator/TransitionOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionOptions.swift 3 | // XCoordinator 4 | // 5 | // Created by Stefan Kofler on 30.04.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | /// 10 | /// TransitionOptions specifies transition customization points defined at the point of triggering a transition. 11 | /// 12 | /// You can use TransitionOptions to define whether or not a transition should be animated. 13 | /// 14 | /// - Note: 15 | /// It might be extended in the future to enable more advanced customization options. 16 | /// 17 | public struct TransitionOptions { 18 | 19 | // MARK: Stored properties 20 | 21 | /// Specifies whether or not the transition should be animated. 22 | public let animated: Bool 23 | 24 | // MARK: Initialization 25 | 26 | /// 27 | /// Creates transition options on the basis of whether or not it should be animated. 28 | /// 29 | /// - Note: 30 | /// Specifying `true` to enable animations does not necessarily lead to an animated transition, 31 | /// if the transition does not support it. 32 | /// 33 | /// - Parameter animated: 34 | /// Whether or not the animation should be animated. 35 | /// 36 | public init(animated: Bool) { 37 | self.animated = animated 38 | } 39 | 40 | // MARK: Static computed properties 41 | 42 | static var `default`: TransitionOptions { 43 | TransitionOptions(animated: true) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/XCoordinator/TransitionPerformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionPerformer.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 13.09.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | /// 10 | /// The TransitionPerformer protocol is used to abstract the route-type specific characteristics of a Coordinator. 11 | /// It keeps type information about its transition performing capabilities. 12 | /// 13 | public protocol TransitionPerformer: Presentable { 14 | 15 | /// The type of transitions that can be executed on the rootViewController. 16 | associatedtype TransitionType: TransitionProtocol 17 | 18 | /// The rootViewController on which transitions are performed. 19 | var rootViewController: TransitionType.RootViewController { get } 20 | 21 | /// 22 | /// Perform a transition. 23 | /// 24 | /// - Warning: 25 | /// Do not use this method directly, but instead try to use the `trigger` 26 | /// method of your coordinator instead wherever possible. 27 | /// 28 | /// - Parameters: 29 | /// - transition: The transition to be performed. 30 | /// - options: The options on how to perform the transition, including the option to enable/disable animations. 31 | /// - completion: The completion handler called once a transition has finished. 32 | /// 33 | func performTransition(_ transition: TransitionType, 34 | with options: TransitionOptions, 35 | completion: PresentationHandler?) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Sources/XCoordinator/TransitionProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionProtocol.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 13.09.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// `TransitionProtocol` is used to abstract any concrete transition implementation. 13 | /// 14 | /// `Transition` is provided as an easily-extensible default transition type implementation. 15 | /// 16 | public protocol TransitionProtocol: TransitionContext { 17 | 18 | /// The type of the rootViewController that can execute the transition. 19 | associatedtype RootViewController: UIViewController 20 | 21 | /// 22 | /// Performs a transition on the given viewController. 23 | /// 24 | /// - Warning: 25 | /// Do not call this method directly. Instead use your coordinator's `performTransition` method or trigger 26 | /// a specified route (latter option is encouraged). 27 | /// 28 | func perform(on rootViewController: RootViewController, 29 | with options: TransitionOptions, 30 | completion: PresentationHandler?) 31 | 32 | // MARK: Always accessible transitions 33 | 34 | /// 35 | /// Creates a compound transition by chaining multiple transitions together. 36 | /// 37 | /// - Parameter transitions: 38 | /// The transitions to be chained to form a combined transition. 39 | /// 40 | static func multiple(_ transitions: [Self]) -> Self 41 | } 42 | 43 | extension TransitionProtocol { 44 | 45 | /// 46 | /// Creates a compound transition by chaining multiple transitions together. 47 | /// 48 | /// - Parameter transitions: 49 | /// The transitions to be chained to form a combined transition. 50 | /// 51 | public static func multiple(_ transitions: Self...) -> Self { 52 | multiple(transitions) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Sources/XCoordinator/UINavigationController+Transition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+Transition.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 27.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UINavigationController { 12 | 13 | func push(_ viewController: UIViewController, 14 | with options: TransitionOptions, 15 | animation: Animation?, 16 | completion: PresentationHandler?) { 17 | 18 | if let animation = animation { 19 | viewController.transitioningDelegate = animation 20 | } 21 | assert(animation == nil || animationDelegate != nil, """ 22 | Animations do not work, if the navigation controller's delegate is not a NavigationAnimationDelegate. 23 | This assertion might fail, if the rootViewController specified in the NavigationCoordinator's 24 | initializer already had a delegate when initializing the NavigationCoordinator. 25 | To set another delegate of a rootViewController in a NavigationCoordinator, have a look at `NavigationCoordinator.delegate`. 26 | """) 27 | 28 | CATransaction.begin() 29 | CATransaction.setCompletionBlock(completion) 30 | 31 | autoreleasepool { 32 | pushViewController(viewController, animated: options.animated) 33 | } 34 | 35 | CATransaction.commit() 36 | } 37 | 38 | func pop(toRoot: Bool, with options: TransitionOptions, animation: Animation?, completion: PresentationHandler?) { 39 | 40 | if let animation = animation { 41 | topViewController?.transitioningDelegate = animation 42 | } 43 | assert(animation == nil || animationDelegate != nil, """ 44 | Animations do not work, if the navigation controller's delegate is not a NavigationAnimationDelegate. 45 | This assertion might fail, if the rootViewController specified in the NavigationCoordinator's 46 | initializer already had a delegate when initializing the NavigationCoordinator. 47 | To set another delegate of a rootViewController in a NavigationCoordinator, have a look at `NavigationCoordinator.delegate`. 48 | """) 49 | 50 | CATransaction.begin() 51 | CATransaction.setCompletionBlock(completion) 52 | 53 | autoreleasepool { 54 | if toRoot { 55 | popToRootViewController(animated: options.animated) 56 | } else { 57 | popViewController(animated: options.animated) 58 | } 59 | } 60 | 61 | CATransaction.commit() 62 | } 63 | 64 | func set(_ viewControllers: [UIViewController], 65 | with options: TransitionOptions, 66 | animation: Animation?, 67 | completion: PresentationHandler?) { 68 | 69 | if let animation = animation { 70 | viewControllers.last?.transitioningDelegate = animation 71 | } 72 | assert(animation == nil || animationDelegate != nil, """ 73 | Animations do not work, if the navigation controller's delegate is not a NavigationAnimationDelegate. 74 | This assertion might fail, if the rootViewController specified in the NavigationCoordinator's 75 | initializer already had a delegate when initializing the NavigationCoordinator. 76 | To set another delegate of a rootViewController in a NavigationCoordinator, have a look at `NavigationCoordinator.delegate`. 77 | """) 78 | 79 | CATransaction.begin() 80 | CATransaction.setCompletionBlock { 81 | if let animation = animation { 82 | viewControllers.forEach { $0.transitioningDelegate = animation } 83 | } 84 | completion?() 85 | } 86 | 87 | autoreleasepool { 88 | setViewControllers(viewControllers, animated: options.animated) 89 | } 90 | 91 | CATransaction.commit() 92 | } 93 | 94 | func pop(to viewController: UIViewController, 95 | options: TransitionOptions, 96 | animation: Animation?, 97 | completion: PresentationHandler?) { 98 | 99 | if let animation = animation { 100 | topViewController?.transitioningDelegate = animation 101 | viewController.transitioningDelegate = animation 102 | } 103 | 104 | assert(animation == nil || animationDelegate != nil, """ 105 | Animations do not work, if the navigation controller's delegate is not a NavigationAnimationDelegate. 106 | This assertion might fail, if the rootViewController specified in the NavigationCoordinator's 107 | initializer already had a delegate when initializing the NavigationCoordinator. 108 | To set another delegate of a rootViewController in a NavigationCoordinator, have a look at `NavigationCoordinator.delegate`. 109 | """) 110 | 111 | CATransaction.begin() 112 | CATransaction.setCompletionBlock(completion) 113 | 114 | autoreleasepool { 115 | _ = popToViewController(viewController, animated: options.animated) 116 | } 117 | 118 | CATransaction.commit() 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /Sources/XCoordinator/UIPageViewController+Transition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIPageViewController+Transition.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 30.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIPageViewController { 12 | func set(_ viewControllers: [UIViewController], 13 | direction: UIPageViewController.NavigationDirection, 14 | with options: TransitionOptions, 15 | completion: PresentationHandler?) { 16 | setViewControllers( 17 | viewControllers, 18 | direction: direction, 19 | animated: options.animated, 20 | completion: { _ in completion?() } 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/XCoordinator/UITabBarController+Transition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITabBarController+Transition.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 27.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UITabBarController { 12 | 13 | func set(_ viewControllers: [UIViewController], 14 | with options: TransitionOptions, 15 | animation: Animation?, 16 | completion: PresentationHandler?) { 17 | 18 | if let animation = animation { 19 | viewControllers.first?.transitioningDelegate = animation 20 | } 21 | assert(animation == nil || animationDelegate != nil, """ 22 | Animations do not work, if the navigation controller's delegate is not a NavigationAnimationDelegate. 23 | This assertion might fail, if the rootViewController specified in the TabBarCoordinator's 24 | initializer already had a delegate when initializing the TabBarCoordinator. 25 | To set another delegate of a rootViewController in a TabBarCoordinator, have a look at `TabBarCoordinator.delegate`. 26 | """) 27 | 28 | CATransaction.begin() 29 | CATransaction.setCompletionBlock(completion) 30 | 31 | autoreleasepool { 32 | setViewControllers(viewControllers, animated: options.animated) 33 | } 34 | 35 | CATransaction.commit() 36 | } 37 | 38 | func select(_ viewController: UIViewController, 39 | with options: TransitionOptions, 40 | animation: Animation?, 41 | completion: PresentationHandler?) { 42 | 43 | if let animation = animation { 44 | viewController.transitioningDelegate = animation 45 | } 46 | assert(animation == nil || animationDelegate != nil, """ 47 | Animations do not work, if the navigation controller's delegate is not a NavigationAnimationDelegate. 48 | This assertion might fail, if the rootViewController specified in the TabBarCoordinator's 49 | initializer already had a delegate when initializing the TabBarCoordinator. 50 | To set another delegate of a rootViewController in a TabBarCoordinator, have a look at `TabBarCoordinator.delegate`. 51 | """) 52 | 53 | CATransaction.begin() 54 | CATransaction.setCompletionBlock(completion) 55 | 56 | autoreleasepool { 57 | selectedViewController = viewController 58 | } 59 | 60 | CATransaction.commit() 61 | } 62 | 63 | func select(index: Int, with options: TransitionOptions, animation: Animation?, completion: PresentationHandler?) { 64 | 65 | if let animation = animation { 66 | viewControllers?[index].transitioningDelegate = animation 67 | } 68 | assert(animation == nil || animationDelegate != nil, """ 69 | Animations do not work, if the navigation controller's delegate is not a NavigationAnimationDelegate. 70 | This assertion might fail, if the rootViewController specified in the TabBarCoordinator's 71 | initializer already had a delegate when initializing the TabBarCoordinator. 72 | To set another delegate of a rootViewController in a TabBarCoordinator, have a look at `TabBarCoordinator.delegate`. 73 | """) 74 | 75 | CATransaction.begin() 76 | CATransaction.setCompletionBlock(completion) 77 | 78 | autoreleasepool { 79 | selectedIndex = index 80 | } 81 | 82 | CATransaction.commit() 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Sources/XCoordinator/UIView+Store.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Store.swift 3 | // XCoordinator 4 | // 5 | // Created by Stefan Kofler on 19.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private var associatedObjectHandle: UInt8 = 0 12 | 13 | extension UIView { 14 | 15 | var strongReferences: [Any] { 16 | get { 17 | objc_getAssociatedObject(self, &associatedObjectHandle) as? [Any] ?? [] 18 | } 19 | set { 20 | objc_setAssociatedObject(self, &associatedObjectHandle, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 21 | } 22 | } 23 | } 24 | 25 | extension UIView { 26 | 27 | @discardableResult 28 | func removePreviewingContext(for _: TransitionType.Type) 29 | -> UIViewControllerPreviewing? { 30 | guard let existingContextIndex = strongReferences 31 | .firstIndex(where: { $0 is CoordinatorPreviewingDelegateObject }), 32 | let contextDelegate = strongReferences 33 | .remove(at: existingContextIndex) as? CoordinatorPreviewingDelegateObject, 34 | let context = contextDelegate.context else { 35 | return nil 36 | } 37 | return context 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/XCoordinator/UIViewController+Transition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Transition.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 28.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIViewController { 12 | 13 | private var topPresentedViewController: UIViewController { 14 | presentedViewController?.topPresentedViewController ?? self 15 | } 16 | 17 | func show(_ viewController: UIViewController, 18 | with options: TransitionOptions, 19 | completion: PresentationHandler?) { 20 | 21 | CATransaction.begin() 22 | CATransaction.setCompletionBlock(completion) 23 | 24 | autoreleasepool { 25 | show(viewController, sender: nil) 26 | } 27 | 28 | CATransaction.commit() 29 | } 30 | 31 | func showDetail(_ viewController: UIViewController, 32 | with options: TransitionOptions, 33 | completion: PresentationHandler?) { 34 | 35 | CATransaction.begin() 36 | CATransaction.setCompletionBlock(completion) 37 | 38 | autoreleasepool { 39 | showDetailViewController(viewController, sender: nil) 40 | } 41 | 42 | CATransaction.commit() 43 | } 44 | 45 | func present(onRoot: Bool, 46 | _ viewController: UIViewController, 47 | with options: TransitionOptions, 48 | animation: Animation?, 49 | completion: PresentationHandler?) { 50 | 51 | if let animation = animation { 52 | viewController.transitioningDelegate = animation 53 | } 54 | let presentingViewController = onRoot 55 | ? self 56 | : topPresentedViewController 57 | presentingViewController.present(viewController, animated: options.animated, completion: completion) 58 | } 59 | 60 | func dismiss(toRoot: Bool, 61 | with options: TransitionOptions, 62 | animation: Animation?, 63 | completion: PresentationHandler?) { 64 | let presentedViewController = topPresentedViewController 65 | if let animation = animation { 66 | presentedViewController.transitioningDelegate = animation 67 | } 68 | let dismissalViewController = toRoot ? self : presentedViewController 69 | dismissalViewController.dismiss(animated: options.animated, completion: completion) 70 | } 71 | 72 | func embed(_ viewController: UIViewController, 73 | in container: Container, 74 | with options: TransitionOptions, 75 | completion: PresentationHandler?) { 76 | container.viewController.addChild(viewController) 77 | 78 | viewController.view.translatesAutoresizingMaskIntoConstraints = false 79 | container.view.addSubview(viewController.view) 80 | 81 | // swiftlint:disable force_unwrapping 82 | NSLayoutConstraint.activate([ 83 | NSLayoutConstraint(item: container.view!, attribute: .leading, relatedBy: .equal, 84 | toItem: viewController.view, attribute: .leading, multiplier: 1, constant: 0), 85 | NSLayoutConstraint(item: container.view!, attribute: .trailing, relatedBy: .equal, 86 | toItem: viewController.view, attribute: .trailing, multiplier: 1, constant: 0), 87 | NSLayoutConstraint(item: container.view!, attribute: .top, relatedBy: .equal, 88 | toItem: viewController.view, attribute: .top, multiplier: 1, constant: 0), 89 | NSLayoutConstraint(item: container.view!, attribute: .bottom, relatedBy: .equal, 90 | toItem: viewController.view, attribute: .bottom, multiplier: 1, constant: 0) 91 | ]) 92 | // swiftlint:enable force_unwrapping 93 | 94 | viewController.didMove(toParent: container.viewController) 95 | 96 | completion?() 97 | } 98 | } 99 | 100 | extension Presentable where Self: UIViewController { 101 | 102 | @available(iOS, introduced: 9.0, deprecated: 13.0, message: "Use `UIContextMenuInteraction` instead.") 103 | func registerPeek( 104 | from sourceView: UIView, 105 | transitionGenerator: @escaping () -> TransitionType, 106 | completion: PresentationHandler?) where TransitionType.RootViewController == Self { 107 | let delegate = CoordinatorPreviewingDelegateObject( 108 | transition: transitionGenerator, 109 | rootViewController: self, 110 | completion: completion 111 | ) 112 | 113 | if let context = sourceView.removePreviewingContext(for: TransitionType.self) { 114 | unregisterForPreviewing(withContext: context) 115 | } 116 | 117 | sourceView.strongReferences.append(delegate) 118 | delegate.context = registerForPreviewing(with: delegate, sourceView: sourceView) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/XCoordinator/UnownedErased+Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnownedErased+Router.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 02.09.19. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// Please use `StrongRouter`, `WeakRouter` or `UnownedRouter` instead. 13 | /// 14 | /// - Note: 15 | /// Use a `StrongRouter`, if you need to hold a router even 16 | /// when it is not in the view hierarchy. 17 | /// Use a `WeakRouter` or `UnownedRouter` when you are accessing 18 | /// any router from the view hierarchy. 19 | /// 20 | @available(iOS, deprecated) 21 | public typealias AnyRouter = UnownedRouter 22 | 23 | /// 24 | /// An `UnownedRouter` is an unowned version of a router object to be used in view controllers or view models. 25 | /// 26 | /// - Note: 27 | /// Do not create an `UnownedRouter` from a `StrongRouter` since `StrongRouter` is only another wrapper 28 | /// and does not represent the might instantly 29 | /// 30 | public typealias UnownedRouter = UnownedErased> 31 | 32 | extension UnownedErased: Presentable where Value: Presentable { 33 | 34 | public var viewController: UIViewController! { 35 | wrappedValue.viewController 36 | } 37 | 38 | public func childTransitionCompleted() { 39 | wrappedValue.childTransitionCompleted() 40 | } 41 | 42 | public func registerParent(_ presentable: Presentable & AnyObject) { 43 | wrappedValue.registerParent(presentable) 44 | } 45 | 46 | public func presented(from presentable: Presentable?) { 47 | wrappedValue.presented(from: presentable) 48 | } 49 | 50 | public func setRoot(for window: UIWindow) { 51 | wrappedValue.setRoot(for: window) 52 | } 53 | 54 | } 55 | 56 | extension UnownedErased: Router where Value: Router { 57 | 58 | public func contextTrigger(_ route: Value.RouteType, with options: TransitionOptions, completion: ContextPresentationHandler?) { 59 | wrappedValue.contextTrigger(route, with: options, completion: completion) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Sources/XCoordinator/UnownedErased.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Paul Kraft on 21.06.19. 6 | // 7 | 8 | import Foundation 9 | 10 | #if swift(>=5.1) 11 | 12 | /// 13 | /// `UnownedErased` is a property wrapper to hold objects with an unowned reference when using type-erasure. 14 | /// 15 | /// Create this wrapper using an initial value and a closure to create the type-erased object. 16 | /// Make sure to not create an `UnownedErased` wrapper for already type-erased objects, 17 | /// since their reference is most likely instantly lost. 18 | /// 19 | @propertyWrapper 20 | public struct UnownedErased { 21 | private var _value: () -> Value 22 | 23 | /// The type-erased or otherwise mapped version of the value being held unowned. 24 | public var wrappedValue: Value { 25 | _value() 26 | } 27 | } 28 | 29 | #else 30 | 31 | /// 32 | /// `UnownedErased` is a property wrapper to hold objects with an unowned reference when using type-erasure. 33 | /// 34 | /// Create this wrapper using an initial value and a closure to create the type-erased object. 35 | /// Make sure to not create an `UnownedErased` wrapper for already type-erased objects, 36 | /// since their reference is most likely instantly lost. 37 | /// 38 | public struct UnownedErased { 39 | private var _value: () -> Value 40 | 41 | /// The type-erased or otherwise mapped version of the value being held unowned. 42 | public var wrappedValue: Value { 43 | _value() 44 | } 45 | } 46 | 47 | #endif 48 | 49 | extension UnownedErased { 50 | 51 | /// 52 | /// Create an `UnownedErased` wrapper using an initial value and a closure to create the type-erased object. 53 | /// Make sure to not create an `UnownedErased` wrapper for already type-erased objects, 54 | /// since their reference is most likely instantly lost. 55 | /// 56 | public init(_ value: Erasable, erase: @escaping (Erasable) -> Value) { 57 | self._value = UnownedErased.createValueClosure(for: value, erase: erase) 58 | } 59 | 60 | /// 61 | /// Set a new value by providing a non-type-erased value and a closure to create the type-erased object. 62 | /// 63 | public mutating func set(_ value: Erasable, erase: @escaping (Erasable) -> Value) { 64 | self._value = UnownedErased.createValueClosure(for: value, erase: erase) 65 | } 66 | 67 | private static func createValueClosure( 68 | for value: Erasable, 69 | erase: @escaping (Erasable) -> Value) -> () -> Value { 70 | { [unowned value] in erase(value) } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/XCoordinator/ViewCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewCoordinator.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 29.07.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// ViewTransition offers transitions common to any `UIViewController` rootViewController. 13 | /// 14 | public typealias ViewTransition = Transition 15 | 16 | /// 17 | /// ViewCoordinator is a base class for custom coordinators with a `UIViewController` rootViewController. 18 | /// 19 | open class ViewCoordinator: BaseCoordinator { 20 | 21 | // MARK: Initialization 22 | 23 | public override init(rootViewController: RootViewController, initialRoute: RouteType? = nil) { 24 | super.init(rootViewController: rootViewController, 25 | initialRoute: initialRoute) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sources/XCoordinator/WeakErased+Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakErased+Router.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 02.09.19. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// 12 | /// A `WeakRouter` is a weak version of a router object to be used in view controllers or view models. 13 | /// 14 | /// - Note: 15 | /// Do not create a `WeakRouter` from a `StrongRouter` since `StrongRouter` is only another wrapper 16 | /// and does not represent the might instantly. 17 | /// Also keep in mind that once the original router object has been deallocated, 18 | /// calling `trigger` on this wrapper will have no effect. 19 | /// 20 | public typealias WeakRouter = WeakErased> 21 | 22 | extension WeakErased: Presentable where Value: Presentable { 23 | 24 | public var viewController: UIViewController! { 25 | wrappedValue?.viewController 26 | } 27 | 28 | public func childTransitionCompleted() { 29 | wrappedValue?.childTransitionCompleted() 30 | } 31 | 32 | public func registerParent(_ presentable: Presentable & AnyObject) { 33 | wrappedValue?.registerParent(presentable) 34 | } 35 | 36 | public func presented(from presentable: Presentable?) { 37 | wrappedValue?.presented(from: presentable) 38 | } 39 | 40 | public func setRoot(for window: UIWindow) { 41 | wrappedValue?.setRoot(for: window) 42 | } 43 | 44 | } 45 | 46 | extension WeakErased: Router where Value: Router { 47 | 48 | public func contextTrigger(_ route: Value.RouteType, with options: TransitionOptions, completion: ContextPresentationHandler?) { 49 | wrappedValue?.contextTrigger(route, with: options, completion: completion) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/XCoordinator/WeakErased.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakErased.swift 3 | // XCoordinator 4 | // 5 | // Created by Paul Kraft on 30.10.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if swift(>=5.1) 12 | 13 | /// 14 | /// `WeakErased` is a property wrapper to hold objects with a weak reference when using type-erasure. 15 | /// 16 | /// Create this wrapper using an initial value and a closure to create the type-erased object. 17 | /// Make sure to not create a `WeakErased` wrapper for already type-erased objects, 18 | /// since their reference is most likely instantly lost. 19 | /// 20 | @propertyWrapper 21 | public struct WeakErased { 22 | private var _value: () -> Value? 23 | 24 | /// The type-erased or otherwise mapped version of the value being held weakly. 25 | public var wrappedValue: Value? { 26 | _value() 27 | } 28 | } 29 | 30 | #else 31 | 32 | /// 33 | /// `WeakErased` is a property wrapper to hold objects with a weak reference when using type-erasure. 34 | /// 35 | /// Create this wrapper using an initial value and a closure to create the type-erased object. 36 | /// Make sure to not create a `WeakErased` wrapper for already type-erased objects, 37 | /// since their reference is most likely instantly lost. 38 | /// 39 | public struct WeakErased { 40 | private var _value: () -> Value? 41 | 42 | /// The type-erased or otherwise mapped version of the value being held weakly. 43 | public var wrappedValue: Value? { 44 | _value() 45 | } 46 | } 47 | 48 | #endif 49 | 50 | extension WeakErased { 51 | 52 | /// 53 | /// Create a `WeakErased` wrapper using an initial value and a closure to create the type-erased object. 54 | /// Make sure to not create a `WeakErased` wrapper for already type-erased objects, 55 | /// since their reference is most likely instantly lost. 56 | /// 57 | public init(_ value: Erasable, erase: @escaping (Erasable) -> Value) { 58 | self._value = WeakErased.createValueClosure(for: value, erase: erase) 59 | } 60 | 61 | /// 62 | /// Set a new value by providing a non-type-erased value and a closure to create the type-erased object. 63 | /// 64 | public mutating func set(_ value: Erasable, erase: @escaping (Erasable) -> Value) { 65 | self._value = WeakErased.createValueClosure(for: value, erase: erase) 66 | } 67 | 68 | private static func createValueClosure( 69 | for value: Erasable, 70 | erase: @escaping (Erasable) -> Value) -> () -> Value? { 71 | { [weak value] in value.map(erase) } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Sources/XCoordinatorCombine/Router+Combine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router+Combine.swift 3 | // XCoordinatorCombine 4 | // 5 | // Created by Paul Kraft on 28.08.19. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | #if canImport(Combine) && canImport(XCoordinator) 10 | 11 | import Combine 12 | import XCoordinator 13 | 14 | public struct PublisherExtension { 15 | public let base: Base 16 | } 17 | 18 | extension Router { 19 | 20 | public var publishers: PublisherExtension { 21 | .init(base: self) 22 | } 23 | 24 | @available(iOS 13.0, tvOS 13.0, *) 25 | public func triggerPublisher( 26 | _ route: RouteType, 27 | with options: TransitionOptions = .init(animated: true) 28 | ) -> Future { 29 | Future { completion in 30 | self.trigger(route, with: options) { 31 | completion(.success(())) 32 | } 33 | } 34 | } 35 | 36 | } 37 | 38 | @available(iOS 13.0, tvOS 13.0, *) 39 | extension PublisherExtension where Base: Router { 40 | 41 | public func trigger( 42 | _ route: Base.RouteType, 43 | with options: TransitionOptions = .init(animated: true) 44 | ) -> Future { 45 | base.triggerPublisher(route, with: options) 46 | } 47 | 48 | } 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /Sources/XCoordinatorRx/Router+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router+Rx.swift 3 | // XCoordinatorRx 4 | // 5 | // Created by Paul Kraft on 28.08.19. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | #if canImport(XCoordinator) && canImport(RxSwift) 10 | 11 | import XCoordinator 12 | import RxSwift 13 | 14 | extension Router { 15 | 16 | /// Use this to access the reactive extensions of `Router` objects. 17 | public var rx: Reactive { 18 | // swiftlint:disable:previous identifier_name 19 | Reactive(self) 20 | } 21 | } 22 | 23 | extension Reactive where Base: Router { 24 | 25 | /// 26 | /// This method transforms the completion block of a router's trigger method into an observable. 27 | /// 28 | /// - Parameter route: 29 | /// The route to be triggered. 30 | /// 31 | /// - Parameter options: 32 | /// Transition options, e.g. defining whether or not the transition should be animated. 33 | /// 34 | /// - Returns: 35 | /// An observable informing about the completion of the transition. 36 | /// 37 | public func trigger(_ route: Base.RouteType, with options: TransitionOptions) -> Observable { 38 | Observable.create { [base] observer -> Disposable in 39 | base.trigger(route, with: options) { 40 | observer.onNext(()) 41 | observer.onCompleted() 42 | } 43 | return Disposables.create() 44 | } 45 | } 46 | 47 | // MARK: Convenience methods 48 | 49 | /// 50 | /// This method transforms the completion block of a router's trigger method into an observable. 51 | /// 52 | /// It uses the default transition options as specified in `Router.trigger`. 53 | /// 54 | /// - Parameter route: 55 | /// The route to be triggered. 56 | /// 57 | /// - Returns: 58 | /// An observable informing about the completion of the transition. 59 | /// 60 | public func trigger(_ route: Base.RouteType) -> Observable { 61 | trigger(route, with: TransitionOptions(animated: true)) 62 | } 63 | } 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /Sources/ios.xcconfig: -------------------------------------------------------------------------------- 1 | SDKROOT = iphoneos 2 | SUPPORTED_PLATFORMS = iphonesimulator iphoneos 3 | IPHONEOS_DEPLOYMENT_TARGET = 12.0 4 | 5 | ARCHS = $(ARCHS_STANDARD) 6 | VALID_ARCHS = $(ARCHS_STANDARD) 7 | 8 | VALIDATE_PRODUCT = YES 9 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks 10 | TARGETED_DEVICE_FAMILY = 1, 2 11 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import XCoordinatorTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += XCoordinatorTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/XCoordinatorTests/AnimationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationTests.swift 3 | // XCoordinator_Example 4 | // 5 | // Created by Paul Kraft on 16.09.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCoordinator 11 | import XCTest 12 | 13 | class AnimationTests: XCTestCase { 14 | 15 | // MARK: Static properties 16 | 17 | static let allTests = [ 18 | ("testPageCoordinator", testPageCoordinator), 19 | ("testSplitCoordinator", testSplitCoordinator), 20 | ("testTabBarCoordinator", testTabBarCoordinator), 21 | ("testViewCoordinator", testViewCoordinator), 22 | ("testNavigationCoordinator", testNavigationCoordinator), 23 | ] 24 | 25 | // MARK: Stored properties 26 | 27 | lazy var window = UIWindow() 28 | 29 | // MARK: Tests 30 | 31 | func testViewCoordinator() { 32 | let coordinator = ViewCoordinator(rootViewController: .init()) 33 | coordinator.setRoot(for: window) 34 | testStandardAnimationsCalled(on: coordinator) 35 | } 36 | 37 | func testSplitCoordinator() { 38 | let coordinator = SplitCoordinator(master: UIViewController(), detail: UIViewController()) 39 | coordinator.setRoot(for: window) 40 | testStandardAnimationsCalled(on: coordinator) 41 | } 42 | 43 | func testPageCoordinator() { 44 | let coordinator = PageCoordinator(pages: [UIViewController()]) 45 | coordinator.setRoot(for: window) 46 | testStandardAnimationsCalled(on: coordinator) 47 | } 48 | 49 | func testTabBarCoordinator() { 50 | let tabs = [UIViewController(), UIViewController(), UIViewController()] 51 | let coordinator = TabBarCoordinator(tabs: tabs) 52 | coordinator.setRoot(for: window) 53 | testStandardAnimationsCalled(on: coordinator) 54 | 55 | testStaticAnimationCalled(on: coordinator, transition: { .select(tabs[1], animation: $0) }) 56 | testInteractiveAnimationCalled(on: coordinator, transition: { .select(tabs[2], animation: $0) }) 57 | 58 | testStaticAnimationCalled(on: coordinator, transition: { .select(index: 1, animation: $0) }) 59 | testInteractiveAnimationCalled(on: coordinator, transition: { .select(index: 2, animation: $0) }) 60 | 61 | testStaticAnimationCalled( 62 | on: coordinator, 63 | transition: { .set([UIViewController(), UIViewController()], animation: $0) } 64 | ) 65 | testInteractiveAnimationCalled( 66 | on: coordinator, 67 | transition: { .set([UIViewController(), UIViewController()], animation: $0) } 68 | ) 69 | } 70 | 71 | func testNavigationCoordinator() { 72 | let coordinator = NavigationCoordinator(root: UIViewController()) 73 | coordinator.setRoot(for: window) 74 | testStandardAnimationsCalled(on: coordinator) 75 | 76 | testStaticAnimationCalled(on: coordinator, transition: { .push(UIViewController(), animation: $0) }) 77 | testStaticAnimationCalled(on: coordinator, transition: { .pop(animation: $0) }) 78 | 79 | testInteractiveAnimationCalled(on: coordinator, transition: { .push(UIViewController(), animation: $0) }) 80 | testInteractiveAnimationCalled(on: coordinator, transition: { .pop(animation: $0) }) 81 | 82 | testStaticAnimationCalled(on: coordinator, transition: { .push(UIViewController(), animation: $0) }) 83 | testStaticAnimationCalled(on: coordinator, transition: { .push(UIViewController(), animation: $0) }) 84 | testStaticAnimationCalled(on: coordinator, transition: { .popToRoot(animation: $0) }) 85 | 86 | testInteractiveAnimationCalled(on: coordinator, transition: { .push(UIViewController(), animation: $0) }) 87 | testInteractiveAnimationCalled(on: coordinator, transition: { .push(UIViewController(), animation: $0) }) 88 | testInteractiveAnimationCalled(on: coordinator, transition: { .popToRoot(animation: $0) }) 89 | 90 | let staticViewControllers = [UIViewController(), UIViewController()] 91 | testStaticAnimationCalled(on: coordinator, transition: { .set(staticViewControllers, animation: $0) }) 92 | testStaticAnimationCalled(on: coordinator, transition: { .pop(to: staticViewControllers[0], animation: $0) }) 93 | 94 | let interactiveViewControllers = [UIViewController(), UIViewController()] 95 | testInteractiveAnimationCalled(on: coordinator, transition: { .set(interactiveViewControllers, animation: $0) }) 96 | testInteractiveAnimationCalled( 97 | on: coordinator, 98 | transition: { .pop(to: interactiveViewControllers[0], animation: $0) } 99 | ) 100 | } 101 | 102 | // MARK: Helpers 103 | 104 | private func testStandardAnimationsCalled(on coordinator: C) where C.TransitionType == Transition { 105 | testStaticAnimationCalled(on: coordinator, transition: { .present(UIViewController(), animation: $0) }) 106 | testStaticAnimationCalled(on: coordinator, transition: { .dismiss(animation: $0) }) 107 | testStaticAnimationCalled( 108 | on: coordinator, 109 | transition: { .multiple(.present(UIViewController(), animation: nil), .dismiss(animation: $0)) } 110 | ) 111 | testStaticAnimationCalled( 112 | on: coordinator, 113 | transition: { .multiple(.present(UIViewController(), animation: $0), .dismiss(animation: .default)) } 114 | ) 115 | 116 | testInteractiveAnimationCalled(on: coordinator, transition: { .present(UIViewController(), animation: $0) }) 117 | testInteractiveAnimationCalled(on: coordinator, transition: { .dismiss(animation: $0) }) 118 | testInteractiveAnimationCalled( 119 | on: coordinator, 120 | transition: { .multiple(.present(UIViewController(), animation: $0), .dismiss(animation: .default)) } 121 | ) 122 | } 123 | 124 | private func testStaticAnimationCalled(on coordinator: C, 125 | transition: (Animation) -> C.TransitionType) { 126 | let animationExpectation = expectation(description: "Animation \(Date().timeIntervalSince1970)") 127 | let completionExpectation = expectation(description: "Completion \(Date().timeIntervalSince1970)") 128 | print(#function, animationExpectation) 129 | let testAnimation = TestAnimation.static(presentation: animationExpectation, dismissal: animationExpectation) 130 | let t = transition(testAnimation) 131 | coordinator.performTransition(t, with: TransitionOptions(animated: true)) { 132 | completionExpectation.fulfill() 133 | } 134 | wait(for: [animationExpectation, completionExpectation], timeout: 3, enforceOrder: true) 135 | asyncWait(for: 0.1) 136 | } 137 | 138 | private func testInteractiveAnimationCalled(on coordinator: C, 139 | transition: (Animation) -> C.TransitionType) { 140 | let animationExpectation = expectation(description: "Animation \(Date().timeIntervalSince1970)") 141 | let completionExpectation = expectation(description: "Completion \(Date().timeIntervalSince1970)") 142 | print(#function, animationExpectation) 143 | let testAnimation = TestAnimation.interactive( 144 | presentation: animationExpectation, 145 | dismissal: animationExpectation 146 | ) 147 | let t = transition(testAnimation) 148 | coordinator.performTransition(t, with: TransitionOptions(animated: true)) { 149 | completionExpectation.fulfill() 150 | _ = testAnimation 151 | } 152 | wait(for: [animationExpectation, completionExpectation], timeout: 3, enforceOrder: true) 153 | asyncWait(for: 0.1) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Tests/XCoordinatorTests/TestAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAnimation.swift 3 | // XCoordinator_Tests 4 | // 5 | // Created by Paul Kraft on 16.09.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import XCoordinator 10 | import XCTest 11 | 12 | class TestAnimation: Animation { 13 | 14 | static func `static`(presentation: XCTestExpectation, dismissal: XCTestExpectation) -> TestAnimation { 15 | TestAnimation( 16 | presentation: TestAnimation.staticTransitionAnimation(for: presentation), 17 | dismissal: TestAnimation.staticTransitionAnimation(for: dismissal) 18 | ) 19 | } 20 | 21 | static func interactive(presentation: XCTestExpectation, dismissal: XCTestExpectation) -> TestAnimation { 22 | TestAnimation( 23 | presentation: TestAnimation.interactiveTransitionAnimation(for: presentation), 24 | dismissal: TestAnimation.interactiveTransitionAnimation(for: dismissal) 25 | ) 26 | } 27 | 28 | private static func interactiveTransitionAnimation(for expectation: XCTestExpectation?) -> TransitionAnimation { 29 | InteractiveTransitionAnimation(duration: 0.1) { 30 | expectation?.fulfill() 31 | $0.completeTransition(true) 32 | } 33 | } 34 | 35 | private static func staticTransitionAnimation(for expectation: XCTestExpectation?) -> TransitionAnimation { 36 | StaticTransitionAnimation(duration: 0.1) { 37 | expectation?.fulfill() 38 | $0.completeTransition(true) 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Tests/XCoordinatorTests/TestRoute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestRoute.swift 3 | // XCoordinatorTests 4 | // 5 | // Created by Paul Kraft on 16.09.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import XCoordinator 10 | 11 | enum TestRoute: Route { 12 | case home 13 | } 14 | -------------------------------------------------------------------------------- /Tests/XCoordinatorTests/TransitionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionTests.swift 3 | // XCoordinatorTests 4 | // 5 | // Created by Paul Kraft on 16.09.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCoordinator 11 | import XCTest 12 | 13 | class TransitionTests: XCTestCase { 14 | 15 | // MARK: Static properties 16 | 17 | static let allTests = [ 18 | ("testPageCoordinator", testPageCoordinator), 19 | ("testSplitCoordinator", testSplitCoordinator), 20 | ("testTabBarCoordinator", testTabBarCoordinator), 21 | ("testViewCoordinator", testViewCoordinator), 22 | ("testNavigationCoordinator", testNavigationCoordinator), 23 | ] 24 | 25 | // MARK: Stored properties 26 | 27 | lazy var window = UIWindow() 28 | 29 | // MARK: Tests 30 | 31 | func testPageCoordinator() { 32 | let pages = [UIViewController(), UIViewController(), UIViewController()] 33 | let coordinator = PageCoordinator(pages: pages) 34 | coordinator.setRoot(for: window) 35 | testStandardTransitions(on: coordinator) 36 | testCompletionCalled(on: coordinator, transition: .set(pages[0], direction: .forward)) 37 | coordinator.rootViewController.isDoubleSided = true 38 | testCompletionCalled(on: coordinator, transition: .set(pages[1], pages[2], direction: .forward)) 39 | } 40 | 41 | func testSplitCoordinator() { 42 | let coordinator = SplitCoordinator(master: UIViewController(), detail: UIViewController()) 43 | coordinator.setRoot(for: window) 44 | testStandardTransitions(on: coordinator) 45 | testCompletionCalled( 46 | on: coordinator, 47 | transition: .multiple(.show(UIViewController()), .showDetail(UIViewController())) 48 | ) 49 | } 50 | 51 | func testTabBarCoordinator() { 52 | let tabs0 = [UIViewController(), UIViewController()] 53 | let coordinator = TabBarCoordinator(tabs: tabs0) 54 | coordinator.setRoot(for: window) 55 | testStandardTransitions(on: coordinator) 56 | let tabs1 = [UIViewController(), UIViewController()] 57 | testCompletionCalled(on: coordinator, transition: .multiple(.set(tabs1), .select(tabs1[1]))) 58 | testCompletionCalled(on: coordinator, transition: .multiple(.set(tabs0), .select(index: 1))) 59 | } 60 | 61 | func testViewCoordinator() { 62 | let coordinator = ViewCoordinator(rootViewController: .init()) 63 | coordinator.setRoot(for: window) 64 | testStandardTransitions(on: coordinator) 65 | } 66 | 67 | func testNavigationCoordinator() { 68 | let coordinator = NavigationCoordinator(root: UIViewController()) 69 | coordinator.setRoot(for: window) 70 | testStandardTransitions(on: coordinator) 71 | testCompletionCalled(on: coordinator, transition: .push(UIViewController())) 72 | testCompletionCalled(on: coordinator, transition: .pop()) 73 | testCompletionCalled(on: coordinator, transition: .push(UIViewController())) 74 | testCompletionCalled(on: coordinator, transition: .popToRoot()) 75 | 76 | let viewControllers = [UIViewController(), UIViewController()] 77 | testCompletionCalled(on: coordinator, transition: .set(viewControllers)) 78 | testCompletionCalled(on: coordinator, transition: .pop(to: viewControllers[0])) 79 | } 80 | 81 | // MARK: Helpers 82 | 83 | private func testStandardTransitions(on coordinator: C) where C.TransitionType == Transition { 84 | print("none") 85 | testCompletionCalled(on: coordinator, transition: .none()) 86 | print("present") 87 | testCompletionCalled(on: coordinator, transition: .present(UIViewController())) 88 | print("dismiss") 89 | testCompletionCalled(on: coordinator, transition: .dismiss()) 90 | print("embed") 91 | testCompletionCalled(on: coordinator, transition: .embed(UIViewController(), in: UIViewController())) 92 | print("multiple(none)") 93 | testCompletionCalled(on: coordinator, transition: .multiple(.none())) 94 | print("multiple(empty)") 95 | testCompletionCalled(on: coordinator, transition: .multiple()) 96 | } 97 | 98 | private func testCompletionCalled(on coordinator: C, transition: C.TransitionType) { 99 | let exp = expectation(description: "\(Date().timeIntervalSince1970)") 100 | DispatchQueue.main.async { 101 | coordinator.performTransition(transition, with: .init(animated: true)) { 102 | exp.fulfill() 103 | } 104 | } 105 | wait(for: [exp], timeout: 3) 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /Tests/XCoordinatorTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | [ 6 | testCase(AnimationTests.allTests), 7 | testCase(TransitionTests.allTests) 8 | ] 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /Tests/XCoordinatorTests/XCText+Extras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCText+Extras.swift 3 | // XCoordinator_Tests 4 | // 5 | // Created by Paul Kraft on 20.11.18. 6 | // Copyright © 2018 QuickBird Studios. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | extension XCTestCase { 13 | 14 | func asyncWait(for timeInterval: TimeInterval) { 15 | let waitExpectation = self.expectation(description: "WAIT \(Date().timeIntervalSince1970)") 16 | DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + timeInterval) { 17 | waitExpectation.fulfill() 18 | } 19 | wait(for: [waitExpectation], timeout: max(timeInterval * 2, 1)) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Tests/XCoordinatorTests/XCoordinatorTests.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "15E74A77-4992-49D7-9071-88779978ED68", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "target" : { 17 | "containerPath" : "container:XCoordinator.xcodeproj", 18 | "identifier" : "XCoordinator::XCoordinatorTests", 19 | "name" : "XCoordinatorTests" 20 | } 21 | } 22 | ], 23 | "version" : 1 24 | } 25 | -------------------------------------------------------------------------------- /XCoordinator.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'XCoordinator' 3 | spec.version = '2.2.1' 4 | spec.license = { :type => 'MIT' } 5 | spec.homepage = 'https://github.com/quickbirdstudios/XCoordinator' 6 | spec.authors = { 'Stefan Kofler' => 'stefan.kofler@quickbirdstudios.com', 'Paul Kraft' => 'pauljohannes.kraft@quickbirdstudios.com' } 7 | spec.summary = 'Navigation framework based on coordinator pattern.' 8 | spec.source = { :git => 'https://github.com/quickbirdstudios/XCoordinator.git', :tag => spec.version } 9 | spec.module_name = 'XCoordinator' 10 | spec.swift_version = '5.1' 11 | spec.ios.deployment_target = '9.0' 12 | spec.tvos.deployment_target = '9.0' 13 | spec.source_files = 'Sources/XCoordinator/*.swift' 14 | spec.default_subspec = 'Core' 15 | 16 | spec.subspec 'Core' do |ss| 17 | ss.source_files = 'Sources/XCoordinator/*.swift' 18 | ss.framework = 'UIKit' 19 | end 20 | 21 | spec.subspec 'RxSwift' do |ss| 22 | ss.dependency 'XCoordinator/Core' 23 | ss.dependency 'RxSwift', '~> 6.1' 24 | ss.source_files = 'Sources/XCoordinatorRx/*.swift' 25 | end 26 | 27 | spec.subspec 'Combine' do |ss| 28 | ss.dependency 'XCoordinator/Core' 29 | ss.source_files = 'Sources/XCoordinatorCombine/*.swift' 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /XCoordinator.xcodeproj/RxSwift_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /XCoordinator.xcodeproj/XCoordinatorCombine_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /XCoordinator.xcodeproj/XCoordinatorRx_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /XCoordinator.xcodeproj/XCoordinatorTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /XCoordinator.xcodeproj/XCoordinator_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /XCoordinator.xcodeproj/xcshareddata/xcschemes/XCoordinator-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 81 | 82 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /XCoordinator.xcodeproj/xcshareddata/xcschemes/XCoordinator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /XCoordinator.xcodeproj/xcshareddata/xcschemes/XCoordinatorCombine.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /XCoordinator.xcodeproj/xcshareddata/xcschemes/XCoordinatorRx.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /XCoordinator.xcodeproj/xcshareddata/xcschemes/XCoordinatorTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 18 | 19 | 20 | 21 | 23 | 29 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/XCoordinator.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.xcoordinator 7 | CFBundleName 8 | XCoordinator 9 | DocSetPlatformFamily 10 | xcoordinator 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/docsets/XCoordinator.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/XCoordinator.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/XCoordinator/fc4fd2d4e5718ea7579bd9022d93af7edecd7b52/docs/docsets/XCoordinator.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/XCoordinator.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/XCoordinator/fc4fd2d4e5718ea7579bd9022d93af7edecd7b52/docs/docsets/XCoordinator.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/XCoordinator.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/XCoordinator/fc4fd2d4e5718ea7579bd9022d93af7edecd7b52/docs/docsets/XCoordinator.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/XCoordinator.docset/Contents/Resources/Documents/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/XCoordinator/fc4fd2d4e5718ea7579bd9022d93af7edecd7b52/docs/docsets/XCoordinator.docset/Contents/Resources/Documents/img/spinner.gif -------------------------------------------------------------------------------- /docs/docsets/XCoordinator.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`.token[href="${location.hash}"]`); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /docs/docsets/XCoordinator.docset/Contents/Resources/Documents/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/docsets/XCoordinator.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/XCoordinator/fc4fd2d4e5718ea7579bd9022d93af7edecd7b52/docs/docsets/XCoordinator.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/XCoordinator.docset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/XCoordinator/fc4fd2d4e5718ea7579bd9022d93af7edecd7b52/docs/docsets/XCoordinator.docset/icon.png -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/XCoordinator/fc4fd2d4e5718ea7579bd9022d93af7edecd7b52/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/XCoordinator/fc4fd2d4e5718ea7579bd9022d93af7edecd7b52/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/XCoordinator/fc4fd2d4e5718ea7579bd9022d93af7edecd7b52/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBirdEng/XCoordinator/fc4fd2d4e5718ea7579bd9022d93af7edecd7b52/docs/img/spinner.gif -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`.token[href="${location.hash}"]`); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | 4 | ], 5 | "source_directory": "/Users/pauljohanneskraft/Documents/QuickBirdStudios/Frameworks/XCoordinator" 6 | } -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Preparation 4 | 5 | set -o pipefail 6 | 7 | # Execution 8 | 9 | swift build \ 10 | -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" \ 11 | -Xswiftc "-target" -Xswiftc "x86_64-apple-ios13.0-simulator" 12 | -------------------------------------------------------------------------------- /scripts/check_docs.sh: -------------------------------------------------------------------------------- 1 | 2 | # Preparation 3 | 4 | cd "$( dirname "$0" )" 5 | undocumented_file_url="../docs/undocumented.json" 6 | 7 | # Execution 8 | 9 | ./docs.sh 10 | 11 | if [ -z "$(git status --untracked-files=no --porcelain)" ]; then 12 | if [[ $(wc -l <$undocumented_file_url) -ge 2 ]]; then 13 | echo "$(cat $undocumented_file_url)" 14 | exit 1 15 | else 16 | exit 0 17 | fi 18 | else 19 | echo "$(git status)\n$(git diff)" 20 | exit 1 21 | fi 22 | -------------------------------------------------------------------------------- /scripts/docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Preparation 4 | 5 | cd "$( dirname "$0" )" 6 | 7 | # Constants 8 | 9 | jazzy_file_url="tmp_jazzy.json" 10 | 11 | # Execution 12 | 13 | cd .. 14 | sourcekitten doc --spm-module XCoordinator -- \ 15 | -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" \ 16 | -Xswiftc "-target" -Xswiftc "x86_64-apple-ios13.0-simulator" \ 17 | > $jazzy_file_url 18 | jazzy --sourcekitten-sourcefile $jazzy_file_url 19 | 20 | # Cleanup 21 | 22 | rm $jazzy_file_url 23 | --------------------------------------------------------------------------------