diff --git a/AppRouter.podspec b/AppRouter.podspec index 8f6a712..c2d8b3b 100755 --- a/AppRouter.podspec +++ b/AppRouter.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "AppRouter" - s.version = "3.0.3" + s.version = "4.0.0" s.summary = "UIViewController creation, navigation, utility methods for easy routing" s.homepage = "https://github.com/MLSDev/AppRouter" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1913257..5e84c11 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. +## [4.0.0](https://github.com/MLSDev/AppRouter/releases/tag/3.0.2) + +AppRouter and Presenter was refactored to provide additional flexibility + + ## [3.0.3](https://github.com/MLSDev/AppRouter/releases/tag/3.0.2) Fixed bug with UIViewController instantiation inside UINavigationController diff --git a/Plists/AppRouter.plist b/Plists/AppRouter.plist index 49ca846..1bcfa6d 100755 --- a/Plists/AppRouter.plist +++ b/Plists/AppRouter.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.0.2 + 4.0.0 CFBundleSignature ???? CFBundleVersion diff --git a/Sources/Core/AppRouter+accessors.swift b/Sources/Core/AppRouter+accessors.swift index 64b25d4..f3e1ad7 100644 --- a/Sources/Core/AppRouter+accessors.swift +++ b/Sources/Core/AppRouter+accessors.swift @@ -3,23 +3,43 @@ import UIKit extension AppRouter { /// Current keyWindow rootViewController - public class var rootViewController : UIViewController? { - get { return AppRouter.window.rootViewController } - set { AppRouter.window.rootViewController = newValue } + open var rootViewController : UIViewController? { + get { return window.rootViewController } + set { window.rootViewController = newValue } } /// Current topmost controller - public class var topViewController : UIViewController? { + open var topViewController : UIViewController? { return topViewController() } /// recursively tries to detect topmost controller /// - parameter startingFrom: Specify controller which will be used as start point for searching /// - returns: returns top-most controller if exists - public class func topViewController(startingFrom base: UIViewController? = AppRouter.rootViewController) -> UIViewController? { + open func topViewController(startingFrom base: UIViewController? = rootViewController) -> UIViewController? { if let topper = base?.toppestControllerFromCurrent() { return topViewController(startingFrom: topper) ?? base } return base } + + // Backwards compatibility part + + /// Current keyWindow rootViewController + public class var rootViewController : UIViewController? { + get { return AppRouter.shared.rootViewController } + set { AppRouter.shared.rootViewController = newValue } + } + + /// Current topmost controller + public class var topViewController : UIViewController? { + return topViewController() + } + + /// recursively tries to detect topmost controller + /// - parameter startingFrom: Specify controller which will be used as start point for searching + /// - returns: returns top-most controller if exists + public class func topViewController(startingFrom base: UIViewController? = rootViewController) -> UIViewController? { + return AppRouter.shared.topViewController(startingFrom: base) + } } public protocol ARToppestControllerProvider { diff --git a/Sources/Core/AppRouter+animations.swift b/Sources/Core/AppRouter+animations.swift index c326466..941f6b8 100644 --- a/Sources/Core/AppRouter+animations.swift +++ b/Sources/Core/AppRouter+animations.swift @@ -1,75 +1,104 @@ import Foundation import UIKit +extension AppRouterType { + var animator: AppRouter.Animators.RootAnimator { return .init(router: self) } +} + extension AppRouter { /// 🚲 Just few example animation methods - public enum animations { - /// Uses UIView.transitionFromView to animate AppRouter.rootViewController change. - /// - /// - parameter controller: controller becoming rootViewController. - /// - parameter options: animation options used to animate transition. - /// - parameter duration: animation duration - /// - parameter callback: called after controller becomes rootViewController - public static func setRootWithViewAnimation(_ controller: UIViewController, options: UIViewAnimationOptions = .transitionFlipFromLeft, duration: TimeInterval = 0.3, callback: Func? = nil) { - if let rootController = AppRouter.rootViewController { - let oldState = UIView.areAnimationsEnabled - UIView.setAnimationsEnabled(false) - UIView.transition(from: rootController.view, to: controller.view, duration: duration, options: options, completion: { state in - AppRouter.rootViewController = controller - UIView.setAnimationsEnabled(oldState) - callback?(state) - }) - } else { - AppRouter.rootViewController = controller - callback?(true) + public enum Animators { + public struct RootAnimator { + let router: AppRouterType + + public func setRoot(controller: UIViewController, animation: AnimationType, callback: Func? = nil) { + switch animation { + case .none: + router.rootViewController = controller + callback?(true) + case .snapshotUpscale(let scaleTo, let opacityTo, let duration): + setRootWithSnapshotAnimation(controller, upscaleTo: scaleTo, opacityTo: opacityTo, duration: duration, callback: callback) + case .view(let options, let duration): + setRootWithViewAnimation(controller, options: options, duration: duration, callback: callback) + case .window(let options, let duration): + setRootWithWindowAnimation(controller, options: options, duration: duration, callback: callback) + } } - } - - /// Uses UIView.transitionWithView to animate AppRouter.rootViewController change. - /// - /// - parameter controller: controller becoming rootViewController - /// - parameter options: animation options used to animate transition. - /// - parameter duration: animation duration - /// - parameter callback: called after controller becomes rootViewController - public static func setRootWithWindowAnimation(_ controller: UIViewController, options: UIViewAnimationOptions = .transitionFlipFromLeft, duration: TimeInterval = 0.3, callback: Func? = nil) { - if let _ = AppRouter.rootViewController { - let oldState = UIView.areAnimationsEnabled - UIView.setAnimationsEnabled(false) - UIView.transition(with: AppRouter.window, duration: duration, options: options, animations: { - AppRouter.rootViewController = controller - }, completion: { state in - UIView.setAnimationsEnabled(oldState) - callback?(state) - }) - } else { - AppRouter.rootViewController = controller - callback?(true) + + + /// Uses UIView.transitionFromView to animate router.rootViewController change. + /// + /// - parameter controller: controller becoming rootViewController. + /// - parameter options: animation options used to animate transition. + /// - parameter duration: animation duration + /// - parameter callback: called after controller becomes rootViewController + public func setRootWithViewAnimation(_ controller: UIViewController, options: UIViewAnimationOptions = .transitionFlipFromLeft, duration: TimeInterval = 0.3, callback: Func? = nil) { + if let rootController = router.rootViewController { + let oldState = UIView.areAnimationsEnabled + UIView.setAnimationsEnabled(false) + UIView.transition(from: rootController.view, to: controller.view, duration: duration, options: options, completion: { state in + self.router.rootViewController = controller + UIView.setAnimationsEnabled(oldState) + callback?(state) + }) + } else { + router.rootViewController = controller + callback?(true) + } + } + + /// Uses UIView.transitionWithView to animate router.rootViewController change. + /// + /// - parameter controller: controller becoming rootViewController + /// - parameter options: animation options used to animate transition. + /// - parameter duration: animation duration + /// - parameter callback: called after controller becomes rootViewController + public func setRootWithWindowAnimation(_ controller: UIViewController, options: UIViewAnimationOptions = .transitionFlipFromLeft, duration: TimeInterval = 0.3, callback: Func? = nil) { + if let _ = router.rootViewController { + let oldState = UIView.areAnimationsEnabled + UIView.setAnimationsEnabled(false) + UIView.transition(with: router.window, duration: duration, options: options, animations: { + self.router.rootViewController = controller + }, completion: { state in + UIView.setAnimationsEnabled(oldState) + callback?(state) + }) + } else { + router.rootViewController = controller + callback?(true) + } + } + + /// Uses UIView.animateWithDuration to animate router.rootViewController change. + /// + /// - parameter controller: controller becoming rootViewController + /// - parameter upscaleTo: final snapshot scale + /// - parameter opacityTo: final snapshot opacity + /// - parameter duration: animation duration + /// - parameter callback: called after controller becomes rootViewController + public func setRootWithSnapshotAnimation(_ controller: UIViewController, upscaleTo: CGFloat = 1.2, opacityTo: Float = 0, duration: TimeInterval = 0.3, callback: Func? = nil) { + if let _ = router.rootViewController, let snapshot:UIView = router.window.snapshotView(afterScreenUpdates: true) { + controller.view.addSubview(snapshot) + router.rootViewController = controller + UIView.animate(withDuration: duration, animations: { + snapshot.layer.opacity = opacityTo + snapshot.layer.transform = CATransform3DMakeScale(upscaleTo, upscaleTo, upscaleTo); + }, completion: { state in + snapshot.removeFromSuperview() + callback?(state) + }) + } else { + router.rootViewController = controller + callback?(true) + } } } - /// Uses UIView.animateWithDuration to animate AppRouter.rootViewController change. - /// - /// - parameter controller: controller becoming rootViewController - /// - parameter upscaleTo: final snapshot scale - /// - parameter opacityTo: final snapshot opacity - /// - parameter duration: animation duration - /// - parameter callback: called after controller becomes rootViewController - public static func setRootWithSnapshotAnimation(_ controller: UIViewController, upscaleTo: CGFloat = 1.2, opacityTo: Float = 0, duration: TimeInterval = 0.3, callback: Func? = nil) { - if let _ = AppRouter.rootViewController, let snapshot:UIView = AppRouter.window.snapshotView(afterScreenUpdates: true) { - controller.view.addSubview(snapshot) - AppRouter.rootViewController = controller - UIView.animate(withDuration: duration, animations: { - snapshot.layer.opacity = opacityTo - snapshot.layer.transform = CATransform3DMakeScale(upscaleTo, upscaleTo, upscaleTo); - }, completion: { state in - snapshot.removeFromSuperview() - callback?(state) - }) - } else { - AppRouter.rootViewController = controller - callback?(true) - } + public enum AnimationType { + case none + case view(options: UIViewAnimationOptions, duration: TimeInterval) + case window(options: UIViewAnimationOptions, duration: TimeInterval) + case snapshotUpscale(scaleTo: CGFloat, opacityTo: Float, duration: TimeInterval) } } } - diff --git a/Sources/Core/AppRouter+instantiations.swift b/Sources/Core/AppRouter+instantiations.swift index e232366..f7329fe 100644 --- a/Sources/Core/AppRouter+instantiations.swift +++ b/Sources/Core/AppRouter+instantiations.swift @@ -25,7 +25,7 @@ extension UIViewController { return _instantiateFromXib(self, xibName: xibName ?? String(describing: self)) } - fileprivate class func _instantiateFromXib(_ : T.Type, xibName : String) ->T { + fileprivate class func _instantiateFromXib(_ : T.Type, xibName : String) -> T { return T(nibName: xibName, bundle: Bundle(for: self)) } } diff --git a/Sources/Core/AppRouter+navigations.swift b/Sources/Core/AppRouter+navigations.swift index 2cbf00c..927e095 100644 --- a/Sources/Core/AppRouter+navigations.swift +++ b/Sources/Core/AppRouter+navigations.swift @@ -38,6 +38,24 @@ extension UIViewController { } return nil } + + /// Pop to previous controller in navigation stack. Do nothing if current is first + /// + /// - parameter animated: Set this value to true to animate the transition + /// - parameter completion: Called after transition ends successfully. + /// - returns: [UIViewCotnroller]? - returns the popped controllers + @discardableResult + public func pop(completion: Func?) -> [UIViewController]? { + return pop(animated: true, completion: completion) + } + + /// Pop to previous controller in navigation stack. Do nothing if current is first + /// + /// - returns: [UIViewCotnroller]? - returns the popped controllers + @discardableResult + public func pop() -> [UIViewController]? { + return pop(animated: true) + } /// Tries to close viewController by popping to previous in navigation stack or by dismissing if presented /// @@ -45,7 +63,7 @@ extension UIViewController { /// - parameter completion: Called after transition ends successfully /// - returns: returns true if able to close @discardableResult - public func close(animated: Bool = true, completion: Func? = nil) -> Bool { + public func close(animated: Bool, completion: Func? = nil) -> Bool { if canPop() { _ = pop(animated: animated, completion: completion) } else if isModal { @@ -57,6 +75,23 @@ extension UIViewController { return true } + /// Tries to close viewController by popping to previous in navigation stack or by dismissing if presented + /// + /// - parameter completion: Called after transition ends successfully + /// - returns: returns true if able to close + @discardableResult + public func close(completion: Func?) -> Bool { + return close(animated: true, completion: completion) + } + + /// Tries to close viewController by popping to previous in navigation stack or by dismissing if presented + /// + /// - returns: returns true if able to close + @discardableResult + public func close() -> Bool { + return close(animated: true, completion: nil) + } + fileprivate func canPop() -> Bool { guard let stack = navigationController?.viewControllers , stack.count > 1 else { return false } guard let first = stack.first , first != self else { return false } diff --git a/Sources/Core/AppRouter+presenter.swift b/Sources/Core/AppRouter+presenter.swift index bd79eb3..412fa25 100644 --- a/Sources/Core/AppRouter+presenter.swift +++ b/Sources/Core/AppRouter+presenter.swift @@ -1,207 +1,356 @@ import Foundation import UIKit -/// Presenter aggregator class -open class ViewControllerPresentConfiguration { - /// Provides target on which presentation will be applied - open var target : ARControllerProvider = ARPresentationTarget.top - - /// Provides controller which will be configured, embedded and presented - open var source : ARControllerProvider = ARPresentationSource.storyboard(initial: true) - - /// Embeds source inside container (UINavigationController, UITabBarController, etc) which will be used for presentation - open var embedder : (T) -> UIViewController? = { $0 } - - /// Configure source controller before presentation - open var configurator: (T) -> () = { _ in } - - /// Declare AppRouter.topViewController to be a **target** provider - open func onTop() -> ViewControllerPresentConfiguration { - target = ARPresentationTarget.top - return self - } - - /// Declare AppRouter.rootViewController to be a **target** provider - open func onRoot() -> ViewControllerPresentConfiguration { - target = ARPresentationTarget.root - return self - } - - /// Declare custom **target** provider - /// - /// - parameter targetBlock: block should return target viewController which will be used for presentation - open func onCustom(_ targetBlock : @escaping () -> UIViewController?) -> ViewControllerPresentConfiguration { - target = ARPresentationTarget.anonymous(targetBlock) - return self - } - - /// Declare **source** provider to take controller from storyboard - /// - /// - parameter name: Storyboard name. Default value: controller type - /// - parameter initial: Set this value if controller is initial in storyboard or it's rootController on initial UINavigationController - open func fromStoryboard(_ name: String? = nil, initial : Bool = true) -> ViewControllerPresentConfiguration { - if let name = name { source = ARPresentationSource.customStoryboard(name: name, inital: initial) } - else { source = ARPresentationSource.storyboard(initial: initial) } - return self - } - - /// Declare **source** provider to take controller from xib - /// - /// - parameter name: Xib name. Default value: contollers type - open func fromXib(_ name: String? = nil) -> ViewControllerPresentConfiguration { - if let name = name { source = ARPresentationSource.customXib(name) } - else { source = ARPresentationSource.xib } - return self - } - - /// Declare **configuration** block which used to configure controller before presentation - /// - /// - parameter configuration: block allows to apply additional configuration before presenting - open func configure(_ configurator: @escaping (T) -> ()) -> ViewControllerPresentConfiguration { - self.configurator = configurator - return self - } - - /// Declare **embedder** provider to embed controller in simple UINavigationController before presentation - /// - /// - parameter navigationController: set custom UINavigationController to be used - open func embedInNavigation(_ navigationController: UINavigationController = UINavigationController()) -> ViewControllerPresentConfiguration { - embedder = { source in - navigationController.viewControllers.append(source) - return navigationController - } - return self +extension AppRouter { + public enum Presenter { + /// Factory for Configurations construction. Can be replaced with your own. + public static var configurationFactory: ARPresentConfigurationFactory = AppRouter.Presenter.DefaultBuilder() } - - /// Declare **embedder** provider to embed controller in UITabBarController before presentation - /// - /// - parameter tabBarController: UITabBarController - used as container of source controller - open func embedInTabBar(_ tabBarController: UITabBarController) -> ViewControllerPresentConfiguration { - embedder = { source in - var originalCollection = tabBarController.viewControllers ?? [] - originalCollection.append(source) - tabBarController.viewControllers = originalCollection - return tabBarController - } - return self - } - - /// Custom anonymous **embedder** provider - /// - /// - parameter embederBlock: block should return UIViewController which will be used as presentation target - open func embedIn(_ embederBlock: @escaping (T) -> UIViewController?) -> ViewControllerPresentConfiguration { - embedder = embederBlock - return self - } - - /// Push current configuration - /// - /// - parameter animated: Set this value to true to animate the transition. - /// - parameter completion: The block to execute after the view controller is pushed. - /// - returns: returns instance provided by `source` provider - @discardableResult - open func push(animated: Bool = true, completion: Func? = nil) -> T? { - guard let sourceController = source.provideController(T.self), let parent = provideEmbeddedController(sourceController) else { debug("error constructing source controller"); return nil } - configurator(sourceController) - guard let targetController = target.provideController(UIViewController.self) else { debug("error fetching target controller"); return nil } - guard let targetNavigation = (targetController as? UINavigationController) ?? targetController.navigationController else { debug("error fetching navigation controller"); return nil } - targetNavigation.pushViewController(parent, animated: animated, completion: completion) - return sourceController - } - - /// Present current configuration - /// - /// - parameter animated: Set this value to true to animate the transition. - /// - parameter completion: The block to execute after the view controller is presented. - /// - returns: returns instance provided by `source` provider - @discardableResult - open func present(animated: Bool = true, completion: Func? = nil) -> T? { - guard let sourceController = source.provideController(T.self), let parent = provideEmbeddedController(sourceController) else { debug("error constructing source controller"); return nil } - configurator(sourceController) - guard let targetController = target.provideController(UIViewController.self) else { debug("error fetching target controller"); return nil } - targetController.present(parent, animated: animated, completion: completion) - return sourceController +} + +/// Used for PresentConfiguration construction +public protocol ARPresentConfigurationFactory { + func buildPresenter() -> AppRouter.Presenter.Configuration +} + +public protocol AppRouterType: class { + var window: UIWindow { get } + var topViewController: UIViewController? { get } + var rootViewController: UIViewController? { get set } +} + +extension AppRouter: AppRouterType {} + +extension AppRouter.Presenter { + /// Presenter aggregator class + open class Configuration { + /// Base router to work with + var router: AppRouterType + + /// Provides target on which presentation will be applied + var targetProvider : () throws -> UIViewController + + /// Provides controller which will be configured, embedded and presented + var sourceProvider : () throws -> T = PresentationSource.storyboard(initial: true).provideController + + /// Embeds source inside container (UINavigationController, UITabBarController, etc) which will be used for presentation + var embedder : (T) throws -> UIViewController = { $0 } + + /// Configure source controller before presentation + var configurator: (T) throws -> () = { _ in } + + /// Declare router.topViewController to be a **target** provider + open func onTop() -> Self { + targetProvider = PresentationTarget.top(router).provideController + return self + } + + /// Declare router.rootViewController to be a **target** provider + open func onRoot() -> Self { + targetProvider = PresentationTarget.root(router).provideController + return self + } + + /// Declare custom **target** provider + /// + /// - parameter provider: block should return target viewController which will be used for presentation + open func on(_ provider: @escaping () throws -> UIViewController) -> Self { + targetProvider = provider + return self + } + + /// Declare **source** provider to take controller from storyboard + /// + /// - parameter name: Storyboard name. Default value: controller type + /// - parameter initial: Set this value if controller is initial in storyboard or it's rootController on initial UINavigationController + open func fromStoryboard(_ name: String? = nil, initial : Bool = true) -> Self { + if let name = name { sourceProvider = PresentationSource.customStoryboard(name: name, inital: initial).provideController } + else { sourceProvider = PresentationSource.storyboard(initial: initial).provideController } + return self + } + + /// Declare **source** provider to take controller from xib + /// + /// - parameter name: Xib name. Default value: contollers type + open func fromXib(_ name: String? = nil) -> Self { + if let name = name { sourceProvider = PresentationSource.customXib(name).provideController } + else { sourceProvider = PresentationSource.xib.provideController } + return self + } + + /// Declare **source** factory to take controller from + /// + /// - parameter provider: closure that providers source controller + open func from(provider: @escaping () throws -> T) -> Self { + sourceProvider = provider + return self + } + + /// Declare **configuration** block which used to configure controller before presentation + /// + /// - parameter configuration: block allows to apply additional configuration before presenting + open func configure(_ configuration: @escaping (T) throws -> ()) -> Self { + self.configurator = configuration + return self + } + + /// Declare **embedder** provider to embed controller in simple UINavigationController before presentation + /// + /// - parameter navigationController: set custom UINavigationController to be used + open func embedInNavigation(_ navigationController: UINavigationController = UINavigationController()) -> Self { + embedder = { source in + navigationController.viewControllers.append(source) + return navigationController + } + return self + } + + /// Declare **embedder** provider to embed controller in UITabBarController before presentation + /// + /// - parameter tabBarController: UITabBarController - used as container of source controller + open func embedInTabBar(_ tabBarController: UITabBarController) -> Self { + embedder = { source in + tabBarController.viewControllers = tabBarController.viewControllers ?? [] + [source] + return tabBarController + } + return self + } + + /// Custom anonymous **embedder** provider + /// + /// - parameter embederBlock: block should return UIViewController which will be used as presentation target + open func embedIn(_ embederBlock: @escaping (T) throws -> UIViewController) -> Self { + embedder = embederBlock + return self + } + + /// Push current configuration + /// + /// - parameter animated: Set this value to true to animate the transition. + /// - parameter completion: The block to execute after the view controller is pushed. + /// - returns: returns instance provided by `source` provider + @discardableResult + open func push(animated: Bool, completion: Func? = nil) -> T? { + do { + let embedded = try provideEmbeddedSourceController() + guard !(embedded.parent is UINavigationController) else { throw Errors.tryingToPushNavigationController } + let targetController = try performTargetConstruction() as UIViewController + let targetNavigation = try targetController as? UINavigationController ?? + targetController.navigationController ?? + Errors.failedToFindNavigationControllerToPushOn.rethrow() + targetNavigation.pushViewController(embedded.parent, animated: animated, completion: completion) + return embedded.child + } catch { + AppRouter.print(error.localizedDescription) + return nil + } + } + + /// Push current configuration + /// + /// - returns: returns instance provided by `source` provider + @discardableResult + open func push() -> T? { + return push(animated: true) + } + + /// Present current configuration + /// + /// - parameter animated: Set this value to true to animate the transition. + /// - parameter completion: The block to execute after the view controller is presented. + /// - returns: returns instance provided by `source` provider + @discardableResult + open func present(animated: Bool, completion: Func? = nil) -> T? { + do { + let embedded = try provideEmbeddedSourceController() + let targetController = try performTargetConstruction() as UIViewController + targetController.present(embedded.parent, animated: animated, completion: completion) + return embedded.child + } catch { + AppRouter.print(error.localizedDescription) + return nil + } + } + + /// Present current configuration + /// + /// - returns: returns instance provided by `source` provider + @discardableResult + open func present() -> T? { + return present(animated: true) + } + + /// Set embedded controller as rootViewController + /// + /// - parameter animation: Animation configuration + /// - parameter completion: The block to execute after the view controller is setted. + /// - returns: returns instance provided by `source` provider + @discardableResult + open func setAsRoot(animation: AppRouter.Animators.AnimationType, completion: Func? = nil) -> T? { + do { + let embedded = try provideEmbeddedSourceController() + router.animator.setRoot(controller: embedded.parent, animation: animation, callback: completion) + return embedded.child + } catch { + AppRouter.print(error.localizedDescription) + return nil + } + } + + /// Set embedded controller as rootViewController with window crossDissolve animation + /// + /// - returns: returns instance provided by `source` provider + @discardableResult + open func setAsRoot() -> T? { + return setAsRoot(animation: .window(options: .transitionCrossDissolve, duration: 0.3)) + } + + /// Provides source controller already configured for use. + /// + /// - returns: controller created from source. + open func provideSourceController() throws -> T { + let sourceController = try performSourceConstruction() + try performConfiguration(for: sourceController) + return sourceController + } + + /// Provides source controller embedded in `embedder` controller and configured for use. + /// + /// - returns: embedded controller. + open func provideEmbeddedSourceController() throws -> (child: T, parent: UIViewController) { + let sourceController = try performSourceConstruction() + let embedded = try performEmbed(for: sourceController) + try performConfiguration(for: sourceController) + return (child: sourceController, parent: embedded) + } + + /// Override point to perform additional logic while constructing source controller + /// + /// - returns: source controller. + open func performSourceConstruction() throws -> T { + return try sourceProvider() + } + + /// Override point to perform additional logic while constructing target controller + /// + /// - returns: target controller. + open func performTargetConstruction() throws -> U { + return try targetProvider() as? U ?? Errors.failedToConstructTargetController.rethrow() + } + + /// Override point to perform additional logic while embedding source controller + /// + /// - returns: parent controller + open func performEmbed(for source: T) throws -> UIViewController { + return try embedder(source) + } + + /// Override point to perform additional logic while configuring source controller + /// + /// - parameter for: source controller to perform configuration on + open func performConfiguration(for source: T) throws -> Void { + if #available(iOS 9.0, *) { + source.loadViewIfNeeded() + } else { + _ = source.view + } + return try configurator(source) + } + + public init(router: AppRouterType = AppRouter.shared) { + self.router = router + self.targetProvider = PresentationTarget.top(router).provideController + } } - /// Provides source controller already configured for use. - /// - /// - returns: controller created from source. - open func provideSourceController() -> T? { - guard let sourceController = source.provideController(T.self) else { debug("error constructing source controller"); return nil } - configurator(sourceController) - return sourceController + public enum Errors: LocalizedError { + case failedToConstructSourceController + case failedToConstructTargetController + case failedToEmbedSourceController + case failedToFindNavigationControllerToPushOn + case tryingToPushNavigationController + + public var errorDescription: String? { + switch self { + case .failedToConstructSourceController: + return "[AppRouter][Presenter] failed to construct source controller." + case .failedToConstructTargetController: + return "[AppRouter][Presenter] failed to construct target controller." + case .failedToEmbedSourceController: + return "[AppRouter][Presenter] failed to embed source controller." + case .failedToFindNavigationControllerToPushOn: + return "[AppRouter][Presenter] failed to find navigation controller (using target provider) to push on." + case .tryingToPushNavigationController: + return "[AppRouter][Presenter] trying to push navigation controller (provided by source provider)." + } + } } - /// Provides source controller embedded in `embedder` controller and configured for use. - /// - /// - returns: embedded controller. - open func provideEmbeddedSourceController() -> UIViewController? { - guard let sourceController = source.provideController(T.self) else { debug("error constructing source controller"); return nil } - guard let embedded = provideEmbeddedController(sourceController) else { return nil } - configurator(sourceController) - return embedded + public enum PresentationTarget { + case top(AppRouterType) + case root(AppRouterType) + case anonymous(() throws -> UIViewController) + public func provideController() throws -> T { + switch self { + case .top(let router): + return try router.topViewController as? T ?? Errors.failedToConstructTargetController.rethrow() + case .root(let router): + return try router.rootViewController as? T ?? Errors.failedToConstructTargetController.rethrow() + case .anonymous(let provider): + return try provider() as? T ?? Errors.failedToConstructTargetController.rethrow() + } + } } - fileprivate func provideEmbeddedController(_ sourceController: T) -> UIViewController? { - guard let parent = embedder(sourceController) else { debug("error embedding controller"); return nil } - return parent + public enum PresentationSource { + case storyboard(initial: Bool) + case xib + case customStoryboard(name: String, inital: Bool) + case customXib(String) + case preconstructed(UIViewController) + case anonymous(() throws -> UIViewController) + public func provideController() throws -> T where T : BundleForClassInstantiable { + switch self { + case .storyboard(let initial): + return try T.instantiate(initial: initial) ?? Errors.failedToConstructSourceController.rethrow() + case .customStoryboard(let name, let initial): + return try T.instantiate(storyboardName: name, initial: initial) ?? Errors.failedToConstructSourceController.rethrow() + case .xib: + return try T.instantiateFromXib() ?? Errors.failedToConstructSourceController.rethrow() + case .customXib(let name): + return try T.instantiateFromXib(name) ?? Errors.failedToConstructSourceController.rethrow() + case .preconstructed(let vc): + return try vc as? T ?? Errors.failedToConstructSourceController.rethrow() + case .anonymous(let provider): + return try provider() as? T ?? Errors.failedToConstructSourceController.rethrow() + } + } } - fileprivate func debug(_ str: String) { - AppRouter.print("#[Presenter<\(T.self)>] " + str) - } -} - -enum ARPresentationTarget : ARControllerProvider{ - case top - case root - case anonymous(() -> UIViewController?) - func provideController(_ type: T.Type) -> T? { - switch self { - case .top: return AppRouter.topViewController() as? T - case .root: return AppRouter.rootViewController as? T - case .anonymous(let provider): return provider() as? T + internal struct DefaultBuilder: ARPresentConfigurationFactory { + func buildPresenter() -> AppRouter.Presenter.Configuration where T : UIViewController { + return .init() } } } -enum ARPresentationSource : ARControllerProvider { - case storyboard(initial: Bool) - case xib - case customStoryboard(name: String, inital: Bool) - case customXib(String) - case preconstructed(UIViewController) - func provideController(_ type: T.Type) -> T? where T : BundleForClassInstantiable { - switch self { - case .storyboard(let initial): return T.instantiate(initial: initial) - case .customStoryboard(let name, let initial): return T.instantiate(storyboardName: name, initial: initial) - case .xib: return T.instantiateFromXib() - case .customXib(let name): return T.instantiateFromXib(name) - case .preconstructed(let vc): return vc as? T - } +extension Error { + internal func rethrow() throws -> T { + throw self } } -/// Used for source and target controller providing -public protocol ARControllerProvider { - /// It should return controller instance of specified type - func provideController(_ type: T.Type) -> T? -} - - /// Workaround to use Self as generic constraint in method public protocol ARControllerConfigurableProtocol : class {} extension UIViewController : ARControllerConfigurableProtocol {} extension ARControllerConfigurableProtocol where Self: UIViewController { /// Presentation configurator. Defaults: -onTop -fromStoryboard - public static func presenter() -> ViewControllerPresentConfiguration { - return ViewControllerPresentConfiguration() + public static func presenter() -> AppRouter.Presenter.Configuration { + return AppRouter.Presenter.configurationFactory.buildPresenter() } - /// Presentation configurator with current instance used as source. Default target - onTop - public func presenter() -> ViewControllerPresentConfiguration { - let configuration : ViewControllerPresentConfiguration = ViewControllerPresentConfiguration() - configuration.source = ARPresentationSource.preconstructed(self) - return configuration + /// Presentation configurator with current instance used as source. Default target - onTop. Warrning - current controller instance will be captured. + public func presenter() -> AppRouter.Presenter.Configuration { + return AppRouter.Presenter.configurationFactory.buildPresenter().from{ self } } } diff --git a/Sources/Core/AppRouter.swift b/Sources/Core/AppRouter.swift index c092052..ac5ecc6 100644 --- a/Sources/Core/AppRouter.swift +++ b/Sources/Core/AppRouter.swift @@ -5,35 +5,37 @@ public typealias Func = (T) -> U /// Namespacing class open class AppRouter { + /// Provides default AppRouter instance + public static var shared = AppRouter() + /// Provides application keyWindow. In normal cases returns UIApplication.sharedApplication().delegate?.window if available or creates new one if not. /// If appDelegate does not implement UIApplicationDelegate.window property - returns UIApplication.sharedApplication().keyWindow - open class var window: UIWindow { - guard let delegate = UIApplication.shared.delegate else { fatalError("no appDelegate found") } - if let windowProperty = delegate.window { - if let window = windowProperty { - return window - } else { - let newWindow = UIWindow(frame: UIScreen.main.bounds) - delegate.perform(#selector(setter: UIApplicationDelegate.window), with: newWindow) - newWindow.makeKeyAndVisible() - return newWindow - } - } else { - guard let window = UIApplication.shared.keyWindow else { fatalError("delegate doesn't implement window property and no UIApplication.sharedApplication().keyWindow available") } - return window - } + public static var window: UIWindow { + return shared.window } - public init() {} + /// Current window which Router work with + open var window: UIWindow { + return windowProvider() + } + open var windowProvider: () -> UIWindow + + public convenience init() { + self.init(windowProvider: WindowProvider.dynamic.window) + } + + public init(windowProvider: @escaping () -> UIWindow) { + self.windowProvider = windowProvider + } /// Defines AppRouter output target - open static var debugOutput: ARDebugOutputProtocol = DebugOutput.nsLog + open static var debugOutput: (String) -> () = DebugOutput.nsLog.debugOutput internal static func print(_ str: String) { - debugOutput.debugOutput(str) + debugOutput(str) } /// Few predefined debugOutput targets - public enum DebugOutput : ARDebugOutputProtocol { + public enum DebugOutput { /// hides output case none @@ -51,9 +53,31 @@ open class AppRouter { } } } -} - -/// AppRouter protocol used to specify proper debug outup mechanic -public protocol ARDebugOutputProtocol { - func debugOutput(_ str: String) + + public enum WindowProvider { + case `static`(UIWindow) + case dynamic + + func window() -> UIWindow { + switch self { + case .static(let window): + return window + case .dynamic: + guard let delegate = UIApplication.shared.delegate else { fatalError("no appDelegate found") } + if let windowProperty = delegate.window { + if let window = windowProperty { + return window + } else { + let newWindow = UIWindow(frame: UIScreen.main.bounds) + delegate.perform(#selector(setter: UIApplicationDelegate.window), with: newWindow) + newWindow.makeKeyAndVisible() + return newWindow + } + } else { + guard let window = UIApplication.shared.keyWindow else { fatalError("delegate doesn't implement window property and no UIApplication.sharedApplication().keyWindow available") } + return window + } + } + } + } } diff --git a/Tests/Accessors/AppRouterAccessorsTests.swift b/Tests/Accessors/AppRouterAccessorsTests.swift index 690c53a..a0817db 100644 --- a/Tests/Accessors/AppRouterAccessorsTests.swift +++ b/Tests/Accessors/AppRouterAccessorsTests.swift @@ -11,7 +11,7 @@ import XCTest class AppRouterAccessorsTests: XCTestCase { - func testHierarchyAccessors() { + func testHierarchyAccessors() throws { XCTAssertNil(AppRouter.rootViewController) XCTAssertNil(AppRouter.topViewController) diff --git a/Tests/Animations/AppRouterAnimationsTests.swift b/Tests/Animations/AppRouterAnimationsTests.swift index 2d9b200..29423d2 100644 --- a/Tests/Animations/AppRouterAnimationsTests.swift +++ b/Tests/Animations/AppRouterAnimationsTests.swift @@ -12,18 +12,18 @@ import XCTest class AppRouterAnimationsTests: XCTestCase { override func setUp() { - AppRouter.rootViewController = nil + AppRouter.shared.rootViewController = nil } func testViewAnimation() { let first = AppRouterPresenterBaseController() let second = AppRouterPresenterAdditionalController() - AppRouter.animations.setRootWithViewAnimation(first, duration: 0) + AppRouter.shared.animator.setRootWithViewAnimation(first, duration: 0) XCTAssertTrue(AppRouter.rootViewController == first) let expectation = self.expectation(description: "") - AppRouter.animations.setRootWithViewAnimation(second, duration: 0, callback: { _ in + AppRouter.shared.animator.setRootWithViewAnimation(second, duration: 0, callback: { _ in XCTAssertTrue(AppRouter.rootViewController == second) expectation.fulfill() }) @@ -34,11 +34,11 @@ class AppRouterAnimationsTests: XCTestCase { let first = AppRouterPresenterBaseController() let second = AppRouterPresenterAdditionalController() - AppRouter.animations.setRootWithWindowAnimation(first, duration: 0) + AppRouter.shared.animator.setRootWithWindowAnimation(first, duration: 0) XCTAssertTrue(AppRouter.rootViewController == first) let expectation = self.expectation(description: "") - AppRouter.animations.setRootWithWindowAnimation(second, duration: 0, callback: { _ in + AppRouter.shared.animator.setRootWithWindowAnimation(second, duration: 0, callback: { _ in XCTAssertTrue(AppRouter.rootViewController == second) expectation.fulfill() }) @@ -49,11 +49,11 @@ class AppRouterAnimationsTests: XCTestCase { let first = AppRouterPresenterBaseController() let second = AppRouterPresenterAdditionalController() - AppRouter.animations.setRootWithSnapshotAnimation(first, duration: 0) + AppRouter.shared.animator.setRootWithSnapshotAnimation(first, duration: 0) XCTAssertTrue(AppRouter.rootViewController == first) let expectation = self.expectation(description: "") - AppRouter.animations.setRootWithSnapshotAnimation(second, duration: 0, callback: { _ in + AppRouter.shared.animator.setRootWithSnapshotAnimation(second, duration: 0, callback: { _ in XCTAssertTrue(AppRouter.rootViewController == second) expectation.fulfill() }) diff --git a/Tests/Presenter/AppRouterPresenterTests.swift b/Tests/Presenter/AppRouterPresenterTests.swift index f72da00..2766e89 100644 --- a/Tests/Presenter/AppRouterPresenterTests.swift +++ b/Tests/Presenter/AppRouterPresenterTests.swift @@ -10,9 +10,9 @@ import XCTest @testable import AppRouter class AppRouterPresenterTests: XCTestCase { - weak var tabBarController: AppRouterPresenterTabBarController? - weak var navController: AppRouterPresenterNavigationController? - weak var baseController: AppRouterPresenterBaseController? + weak var tabBarController: AppRouterPresenterTabBarController! + weak var navController: AppRouterPresenterNavigationController! + weak var baseController: AppRouterPresenterBaseController! override func setUp() { tabBarController = AppRouterPresenterTabBarController.instantiate(storyboardName: "AppRouterPresenterControllers", initial: true) @@ -29,118 +29,121 @@ class AppRouterPresenterTests: XCTestCase { } func testPresenterUtilityTargetMethods() { - let presenter = UIViewController.presenter() - _ = presenter.onTop() - XCTAssertTrue(presenter.target.provideController(UIViewController.self) == baseController) - _ = presenter.onRoot() - XCTAssertTrue(presenter.target.provideController(UIViewController.self) == tabBarController) - _ = presenter.onCustom({ self.baseController }) - XCTAssertTrue(presenter.target.provideController(UIViewController.self) == baseController) + XCTAssertTrue(try UIViewController.presenter().onTop().performTargetConstruction() == baseController) + XCTAssertTrue(try UIViewController.presenter().onRoot().performTargetConstruction() == tabBarController) + XCTAssertTrue(try UIViewController.presenter().on{ self.baseController }.performTargetConstruction() == baseController) } func testPresenterUtilitySourceMethods() { - let presenter = AppRouterPresenterAdditionalController.presenter() - _ = presenter.fromXib() - XCTAssertNotNil(presenter.source.provideController(AppRouterPresenterAdditionalController.self)) - _ = presenter.fromXib("AppRouterPresenterAdditionalController") - XCTAssertNotNil(presenter.source.provideController(AppRouterPresenterAdditionalController.self)) - _ = presenter.fromStoryboard("AppRouterPresenterControllers", initial: false) - XCTAssertNotNil(presenter.source.provideController(AppRouterPresenterAdditionalController.self)) - - let initialPresenter = StoryboardWithInitialViewController.presenter() - _ = initialPresenter.fromStoryboard() - XCTAssertNotNil(initialPresenter.source.provideController(StoryboardWithInitialViewController.self)) + XCTAssertNotNil(try AppRouterPresenterAdditionalController.presenter().fromXib().performSourceConstruction()) + XCTAssertNotNil(try AppRouterPresenterAdditionalController.presenter().fromXib("AppRouterPresenterAdditionalController").performSourceConstruction()) + XCTAssertNotNil(try AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterPresenterControllers", initial: false).performSourceConstruction()) + XCTAssertNotNil(try StoryboardWithInitialViewController.presenter().fromStoryboard().performSourceConstruction()) } - + func testPresenterUtilityConfigurationMethods() { - let presenter = AppRouterPresenterBaseController.presenter() - guard let base = baseController else { return XCTFail() } - presenter.configurator(base) - XCTAssertFalse(base.initialized) - _ = presenter.configure({ $0.initialized = true }) - presenter.configurator(base) - XCTAssertTrue(base.initialized) + XCTAssertFalse(try baseController.presenter().provideSourceController().initialized) + XCTAssertTrue(try baseController.presenter().configure({ $0.initialized = true }).provideSourceController().initialized) } - func testPresenterProvideSourceController() { - let presenter = AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterPresenterControllers", initial: false) - guard let base = baseController else { return XCTFail() } - XCTAssertFalse(base.initialized) - _ = presenter.configure({ $0.initialized = true }) - let source = presenter.provideSourceController() - XCTAssertTrue(source?.initialized == true) + func testPresenterProvideSourceController() throws { + XCTAssertFalse(baseController.initialized) + let presenter = AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterPresenterControllers", initial: false).configure({ $0.initialized = true }) + XCTAssertTrue(try presenter.provideSourceController().initialized == true) } - func testPresenterProvideEmbeddedSourceController() { - let presenter = AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterPresenterControllers", initial: false) + func testPresenterProvideEmbeddedSourceController() throws { + XCTAssertFalse(baseController.initialized) let nav = UINavigationController() - guard let base = baseController else { return XCTFail() } - XCTAssertFalse(base.initialized) - _ = presenter.configure({ - $0.initialized = true - $0.navigationController?.title = "TestTitle" - }).embedInNavigation(nav) - let embedded = presenter.provideEmbeddedSourceController() - XCTAssertTrue(embedded === nav) - guard let embeddedNav = embedded as? UINavigationController else { return XCTFail() } - guard let first = embeddedNav.visibleViewController as? AppRouterPresenterAdditionalController else { return XCTFail() } - XCTAssertTrue(first.initialized == true) + let presenter = AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterPresenterControllers", initial: false) + .configure({ + $0.initialized = true + $0.navigationController?.title = "TestTitle" + }).embedInNavigation(nav) + + let embedded = try presenter.provideEmbeddedSourceController() + XCTAssertTrue(embedded.parent === nav) + guard let embeddedNav = embedded.parent as? UINavigationController else { return XCTFail() } + guard let visible = embeddedNav.visibleViewController as? AppRouterPresenterAdditionalController else { return XCTFail() } + XCTAssertTrue(visible.initialized == true) XCTAssertTrue(embeddedNav.title == "TestTitle") + XCTAssertTrue(embedded.child === visible) } - func testPresenterUtilityEmbeddingMethods() { - let presenter = AppRouterPresenterAdditionalController.presenter() + func testPresenterUtilityEmbedInNavigation() throws { let controller = AppRouterPresenterAdditionalController() - _ = presenter.embedInNavigation() - guard let navigation = presenter.embedder(controller) as? UINavigationController else { return XCTFail() } + let presenter = controller.presenter().embedInNavigation() + let embed = try presenter.provideEmbeddedSourceController() + guard let navigation = embed.parent as? UINavigationController else { return XCTFail() } XCTAssertTrue(navigation.topViewController == controller) - - guard let customNavigation = navController else { return XCTFail() } - _ = presenter.embedInNavigation(customNavigation) - guard let embeddedCustom = presenter.embedder(controller) as? UINavigationController else { return XCTFail() } - XCTAssertTrue(embeddedCustom is AppRouterPresenterNavigationController) - XCTAssertTrue(embeddedCustom.topViewController == controller) - - - guard let customTabBar = tabBarController else { return XCTFail() } - _ = presenter.embedInTabBar(customTabBar) - guard let embeddedCustomTabBar = presenter.embedder(controller) as? UITabBarController else { return XCTFail() } - XCTAssertTrue(embeddedCustomTabBar is AppRouterPresenterTabBarController) - XCTAssertTrue(embeddedCustomTabBar.viewControllers?.last == controller) - - _ = presenter.embedIn({ $0 }) - XCTAssertTrue(presenter.embedder(controller) == controller) - + } + + func testPresenterUtilityEmbedInCustomNavigation() throws { + let controller = AppRouterPresenterAdditionalController() + let navigation = UINavigationController() + let presenter = controller.presenter().embedInNavigation(navigation) + let embed = try presenter.provideEmbeddedSourceController() + guard let nav = embed.parent as? UINavigationController else { return XCTFail() } + XCTAssertTrue(navigation.topViewController == controller) + XCTAssertTrue(nav == navigation) + XCTAssertTrue(embed.child == controller) + } + + func testPresenterUtilityEmbedInCustomTabBar() throws { + let controller = AppRouterPresenterAdditionalController() + let tabBar = UITabBarController() + let presenter = controller.presenter().embedInTabBar(tabBar) + let embed = try presenter.provideEmbeddedSourceController() + guard let tab = embed.parent as? UITabBarController else { return XCTFail() } + XCTAssertTrue(tab.viewControllers?.last == controller) + XCTAssertTrue(tab == tabBar) + XCTAssertTrue(embed.child == controller) + } + + func testPresenterUtilityEmbedInCustomProvider() { + let controller = AppRouterPresenterAdditionalController() + XCTAssertTrue(try controller.presenter().embedIn({ $0 }).provideEmbeddedSourceController().parent == controller) + } + + func testPresenterUtilityCustom() throws { let customPresenter = AppRouterPresenterAdditionalController.presenter() .fromStoryboard("AppRouterPresenterControllers", initial: false) .embedIn({ self.navController?.viewControllers = [$0]; return self.navController }) .configure({ $0.initialized = true }) - guard let embeddedController = customPresenter.provideEmbeddedSourceController() as? AppRouterPresenterNavigationController else { return XCTFail() } + guard let embeddedController = try customPresenter.provideEmbeddedSourceController().parent as? AppRouterPresenterNavigationController else { return XCTFail() } XCTAssertTrue(embeddedController == navController) XCTAssertTrue((embeddedController.topViewController as? AppRouterPresenterAdditionalController)?.initialized == true) - XCTAssertNil(AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterInstantiationsTests", initial: true).provideEmbeddedSourceController()) + XCTAssertNil(try? AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterInstantiationsTests", initial: true).provideEmbeddedSourceController()) } - func testPresenterPresent() { + func testPresenterPresent() throws { XCTAssertTrue(AppRouter.topViewController == baseController) - guard let presented = AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterPresenterControllers", initial: false).configure({ $0.initialized = true }).present(animated: false) else { return XCTFail() } + let presented = AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterPresenterControllers", initial: false).configure({ $0.initialized = true }).present(animated: false) XCTAssertTrue(AppRouter.topViewController == presented) XCTAssertTrue(baseController?.presentedViewController == presented) - XCTAssertTrue(presented.initialized) + XCTAssertTrue(presented?.initialized == true) XCTAssertNil(AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterInstantiationsTests", initial: true).present()) } - func testPresenterPush() { + func testPresenterPush() throws { XCTAssertTrue(AppRouter.topViewController == baseController) - guard let pushed = AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterPresenterControllers", initial: false).configure({ $0.initialized = true }).push(animated: false) else { return XCTFail() } + let pushed = AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterPresenterControllers", initial: false).configure({ $0.initialized = true }).push(animated: false) XCTAssertTrue(AppRouter.topViewController == pushed) - XCTAssertTrue(pushed.navigationController == navController) - XCTAssertTrue(pushed.initialized) + XCTAssertTrue(pushed?.navigationController == navController) + XCTAssertTrue(pushed?.initialized == true) XCTAssertNil(AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterInstantiationsTests").push()) } + func testPresenterSetAsRoot() throws { + XCTAssertTrue(AppRouter.topViewController == baseController) + let presented = AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterPresenterControllers", initial: false).configure({ $0.initialized = true }).setAsRoot(animation: .none) + XCTAssertTrue(AppRouter.rootViewController == presented) + XCTAssertTrue(presented?.initialized == true) + XCTAssertNil(AppRouterPresenterAdditionalController.presenter().fromStoryboard("AppRouterInstantiationsTests", initial: true).setAsRoot()) + } + func testPresenterOnInstance() { let controller = UIViewController() - XCTAssertTrue(controller.presenter().provideSourceController() == controller) + XCTAssertTrue(try controller.presenter().provideSourceController() == controller) } }