diff --git a/README.md b/README.md index ad4252c..f7f1b39 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,8 @@ You can find an example app [here](https://github.com/devxoul/URLNavigator/tree/ 2. Register to an IoC container: ```swift - container.register(NavigatorType.self) { _ in Navigator() } // Swinject - let navigator = container.resolve(NavigatorType.self)! + container.register(NavigatorProtocol.self) { _ in Navigator() } // Swinject + let navigator = container.resolve(NavigatorProtocol.self)! ``` 3. Inject dependency from a composition root. @@ -123,7 +123,7 @@ I'd prefer using separated URL map file. ```swift struct URLNavigationMap { - static func initialize(navigator: NavigatorType) { + static func initialize(navigator: NavigatorProtocol) { navigator.register("myapp://user/") { ... } navigator.register("myapp://post/") { ... } navigator.handle("myapp://alert") { ... } diff --git a/Sources/URLNavigator/Navigator.swift b/Sources/URLNavigator/Navigator.swift index fe6de9e..a202eee 100644 --- a/Sources/URLNavigator/Navigator.swift +++ b/Sources/URLNavigator/Navigator.swift @@ -5,25 +5,46 @@ import UIKit import URLMatcher #endif -open class Navigator: NavigatorType { +public typealias URLPattern = String +public typealias ViewControllerFactory = (_ url: URLConvertible, _ values: [String: Any], _ context: Any?) -> UIViewController? +public typealias URLOpenHandlerFactory = (_ url: URLConvertible, _ values: [String: Any], _ context: Any?) -> Bool +public typealias URLOpenHandler = () -> Bool + +open class Navigator: NavigatorProtocol { + + // MARK: Properties + public let matcher = URLMatcher() open weak var delegate: NavigatorDelegate? private var viewControllerFactories = [URLPattern: ViewControllerFactory]() private var handlerFactories = [URLPattern: URLOpenHandlerFactory]() + + // MARK: Initializing + public init() { // ⛵ I'm a Navigator! } + + // MARK: Registering URLs + + /// Registers a view controller factory to the URL pattern. open func register(_ pattern: URLPattern, _ factory: @escaping ViewControllerFactory) { self.viewControllerFactories[pattern] = factory } + /// Registers an URL open handler to the URL pattern. open func handle(_ pattern: URLPattern, _ factory: @escaping URLOpenHandlerFactory) { self.handlerFactories[pattern] = factory } + /// Returns a matching view controller from the specified URL. + /// + /// - parameter url: An URL to find view controllers. + /// + /// - returns: A match view controller or `nil` if not matched. open func viewController(for url: URLConvertible, context: Any? = nil) -> UIViewController? { let urlPatterns = Array(self.viewControllerFactories.keys) guard let match = self.matcher.match(url, from: urlPatterns) else { return nil } @@ -31,11 +52,93 @@ open class Navigator: NavigatorType { return factory(url, match.values, context) } - open func handler(for url: URLConvertible, context: Any?) -> URLOpenHandler? { + /// Returns a matching URL handler from the specified URL. + /// + /// - parameter url: An URL to find url handlers. + /// + /// - returns: A matching handler factory or `nil` if not matched. + open func handler(for url: URLConvertible, context: Any? = nil) -> URLOpenHandler? { let urlPatterns = Array(self.handlerFactories.keys) guard let match = self.matcher.match(url, from: urlPatterns) else { return nil } guard let handler = self.handlerFactories[match.pattern] else { return nil } return { handler(url, match.values, context) } } + + + // MARK: Push + + /// Pushes a matching view controller to the navigation controller stack. + /// + /// - note: It is not a good idea to use this method directly because this method requires all + /// parameters. This method eventually gets called when pushing a view controller with + /// an URL, so it's recommended to implement this method only for mocking. + @discardableResult + open func push(_ url: URLConvertible, context: Any? = nil, from: UINavigationControllerType? = nil, animated: Bool = true) -> UIViewController? { + guard let viewController = self.viewController(for: url, context: context) else { return nil } + return self.push(viewController, from: from, animated: animated) + } + + /// Pushes the view controller to the navigation controller stack. + /// + /// - note: It is not a good idea to use this method directly because this method requires all + /// parameters. This method eventually gets called when pushing a view controller, so + /// it's recommended to implement this method only for mocking. + @discardableResult + open func push(_ viewController: UIViewController, from: UINavigationControllerType? = nil, animated: Bool = true) -> UIViewController? { + guard (viewController is UINavigationController) == false else { return nil } + guard let navigationController = from ?? UIViewController.topMost?.navigationController else { return nil } + guard self.delegate?.shouldPush(viewController: viewController, from: navigationController) != false else { return nil } + navigationController.pushViewController(viewController, animated: animated) + return viewController + } + + + // MARK: Present + + /// Presents a matching view controller. + /// + /// - note: It is not a good idea to use this method directly because this method requires all + /// parameters. This method eventually gets called when presenting a view controller with + /// an URL, so it's recommended to implement this method only for mocking. + @discardableResult + open func present(_ url: URLConvertible, context: Any? = nil, wrap: UINavigationController.Type? = nil, from: UIViewControllerType? = nil, animated: Bool = true, completion: (() -> Void)? = nil) -> UIViewController? { + guard let viewController = self.viewController(for: url, context: context) else { return nil } + return self.present(viewController, wrap: wrap, from: from, animated: animated, completion: completion) + } + + /// Presents the view controller. + /// + /// - note: It is not a good idea to use this method directly because this method requires all + /// parameters. This method eventually gets called when presenting a view controller, so + /// it's recommended to implement this method only for mocking. + @discardableResult + open func present(_ viewController: UIViewController, wrap: UINavigationController.Type? = nil, from: UIViewControllerType? = nil, animated: Bool = true, completion: (() -> Void)? = nil) -> UIViewController? { + guard let fromViewController = from ?? UIViewController.topMost else { return nil } + + let viewControllerToPresent: UIViewController + if let navigationControllerClass = wrap, (viewController is UINavigationController) == false { + viewControllerToPresent = navigationControllerClass.init(rootViewController: viewController) + } else { + viewControllerToPresent = viewController + } + + guard self.delegate?.shouldPresent(viewController: viewController, from: fromViewController) != false else { return nil } + fromViewController.present(viewControllerToPresent, animated: animated, completion: completion) + return viewController + } + + + // MARK: Open + + /// Executes an URL open handler. + /// + /// - note: It is not a good idea to use this method directly because this method requires all + /// parameters. This method eventually gets called when opening an url, so it's + /// recommended to implement this method only for mocking. + @discardableResult + open func open(_ url: URLConvertible, context: Any? = nil) -> Bool { + guard let handler = self.handler(for: url, context: context) else { return false } + return handler() + } } #endif diff --git a/Sources/URLNavigator/NavigatorProtocol.swift b/Sources/URLNavigator/NavigatorProtocol.swift new file mode 100644 index 0000000..289847f --- /dev/null +++ b/Sources/URLNavigator/NavigatorProtocol.swift @@ -0,0 +1,60 @@ +#if os(iOS) || os(tvOS) +import UIKit + +#if !COCOAPODS +import URLMatcher +#endif + +public protocol NavigatorProtocol: class { + var delegate: NavigatorDelegate? { get set } + + func register(_ pattern: URLPattern, _ factory: @escaping ViewControllerFactory) + func handle(_ pattern: URLPattern, _ factory: @escaping URLOpenHandlerFactory) + + func viewController(for url: URLConvertible, context: Any?) -> UIViewController? + func handler(for url: URLConvertible, context: Any?) -> URLOpenHandler? + + @discardableResult + func push(_ url: URLConvertible, context: Any?, from: UINavigationControllerType?, animated: Bool) -> UIViewController? + + @discardableResult + func push(_ viewController: UIViewController, from: UINavigationControllerType?, animated: Bool) -> UIViewController? + + @discardableResult + func present(_ url: URLConvertible, context: Any?, wrap: UINavigationController.Type?, from: UIViewControllerType?, animated: Bool, completion: (() -> Void)?) -> UIViewController? + + @discardableResult + func present(_ viewController: UIViewController, wrap: UINavigationController.Type?, from: UIViewControllerType?, animated: Bool, completion: (() -> Void)?) -> UIViewController? + + @discardableResult + func open(_ url: URLConvertible, context: Any?) -> Bool +} + + +// MARK: - Optional Parameters + +public extension NavigatorProtocol { + func viewController(for url: URLConvertible, context: Any? = nil) -> UIViewController? { + return self.viewController(for: url, context: context) + } + + func handler(for url: URLConvertible, context: Any? = nil) -> URLOpenHandler? { + return self.handler(for: url, context: context) + } + + @discardableResult + func push(_ url: URLConvertible, context: Any? = nil, from: UINavigationControllerType? = nil, animated: Bool = true) -> UIViewController? { + return self.push(url, context: context, from: from, animated: animated) + } + + @discardableResult + func present(_ url: URLConvertible, context: Any? = nil, wrap: UINavigationController.Type? = nil, from: UIViewControllerType? = nil, animated: Bool = true, completion: (() -> Void)? = nil) -> UIViewController? { + return self.present(url, context: context, wrap: wrap, from: from, animated: animated, completion: completion) + } + + @discardableResult + func open(_ url: URLConvertible, context: Any? = nil) -> Bool { + return self.open(url, context: context) + } +} +#endif diff --git a/Sources/URLNavigator/NavigatorType.swift b/Sources/URLNavigator/NavigatorType.swift deleted file mode 100644 index a5a4dde..0000000 --- a/Sources/URLNavigator/NavigatorType.swift +++ /dev/null @@ -1,163 +0,0 @@ -#if os(iOS) || os(tvOS) -import UIKit - -#if !COCOAPODS -import URLMatcher -#endif - -public typealias URLPattern = String -public typealias ViewControllerFactory = (_ url: URLConvertible, _ values: [String: Any], _ context: Any?) -> UIViewController? -public typealias URLOpenHandlerFactory = (_ url: URLConvertible, _ values: [String: Any], _ context: Any?) -> Bool -public typealias URLOpenHandler = () -> Bool - -public protocol NavigatorType { - var matcher: URLMatcher { get } - var delegate: NavigatorDelegate? { get set } - - /// Registers a view controller factory to the URL pattern. - func register(_ pattern: URLPattern, _ factory: @escaping ViewControllerFactory) - - /// Registers an URL open handler to the URL pattern. - func handle(_ pattern: URLPattern, _ factory: @escaping URLOpenHandlerFactory) - - /// Returns a matching view controller from the specified URL. - /// - /// - parameter url: An URL to find view controllers. - /// - /// - returns: A match view controller or `nil` if not matched. - func viewController(for url: URLConvertible, context: Any?) -> UIViewController? - - /// Returns a matching URL handler from the specified URL. - /// - /// - parameter url: An URL to find url handlers. - /// - /// - returns: A matching handler factory or `nil` if not matched. - func handler(for url: URLConvertible, context: Any?) -> URLOpenHandler? - - /// Pushes a matching view controller to the navigation controller stack. - /// - /// - note: It is not a good idea to use this method directly because this method requires all - /// parameters. This method eventually gets called when pushing a view controller with - /// an URL, so it's recommended to implement this method only for mocking. - @discardableResult - func pushURL(_ url: URLConvertible, context: Any?, from: UINavigationControllerType?, animated: Bool) -> UIViewController? - - /// Pushes the view controller to the navigation controller stack. - /// - /// - note: It is not a good idea to use this method directly because this method requires all - /// parameters. This method eventually gets called when pushing a view controller, so - /// it's recommended to implement this method only for mocking. - @discardableResult - func pushViewController(_ viewController: UIViewController, from: UINavigationControllerType?, animated: Bool) -> UIViewController? - - /// Presents a matching view controller. - /// - /// - note: It is not a good idea to use this method directly because this method requires all - /// parameters. This method eventually gets called when presenting a view controller with - /// an URL, so it's recommended to implement this method only for mocking. - @discardableResult - func presentURL(_ url: URLConvertible, context: Any?, wrap: UINavigationController.Type?, from: UIViewControllerType?, animated: Bool, completion: (() -> Void)?) -> UIViewController? - - /// Presents the view controller. - /// - /// - note: It is not a good idea to use this method directly because this method requires all - /// parameters. This method eventually gets called when presenting a view controller, so - /// it's recommended to implement this method only for mocking. - @discardableResult - func presentViewController(_ viewController: UIViewController, wrap: UINavigationController.Type?, from: UIViewControllerType?, animated: Bool, completion: (() -> Void)?) -> UIViewController? - - /// Executes an URL open handler. - /// - /// - note: It is not a good idea to use this method directly because this method requires all - /// parameters. This method eventually gets called when opening an url, so it's - /// recommended to implement this method only for mocking. - @discardableResult - func openURL(_ url: URLConvertible, context: Any?) -> Bool -} - - -// MARK: - Protocol Requirements - -extension NavigatorType { - public func viewController(for url: URLConvertible) -> UIViewController? { - return self.viewController(for: url, context: nil) - } - - public func handler(for url: URLConvertible) -> URLOpenHandler? { - return self.handler(for: url, context: nil) - } - - @discardableResult - public func pushURL(_ url: URLConvertible, context: Any? = nil, from: UINavigationControllerType? = nil, animated: Bool = true) -> UIViewController? { - guard let viewController = self.viewController(for: url, context: context) else { return nil } - return self.pushViewController(viewController, from: from, animated: animated) - } - - @discardableResult - public func pushViewController(_ viewController: UIViewController, from: UINavigationControllerType?, animated: Bool) -> UIViewController? { - guard (viewController is UINavigationController) == false else { return nil } - guard let navigationController = from ?? UIViewController.topMost?.navigationController else { return nil } - guard self.delegate?.shouldPush(viewController: viewController, from: navigationController) != false else { return nil } - navigationController.pushViewController(viewController, animated: animated) - return viewController - } - - @discardableResult - public func presentURL(_ url: URLConvertible, context: Any? = nil, wrap: UINavigationController.Type? = nil, from: UIViewControllerType? = nil, animated: Bool = true, completion: (() -> Void)? = nil) -> UIViewController? { - guard let viewController = self.viewController(for: url, context: context) else { return nil } - return self.presentViewController(viewController, wrap: wrap, from: from, animated: animated, completion: completion) - } - - @discardableResult - public func presentViewController(_ viewController: UIViewController, wrap: UINavigationController.Type?, from: UIViewControllerType?, animated: Bool, completion: (() -> Void)?) -> UIViewController? { - guard let fromViewController = from ?? UIViewController.topMost else { return nil } - - let viewControllerToPresent: UIViewController - if let navigationControllerClass = wrap, (viewController is UINavigationController) == false { - viewControllerToPresent = navigationControllerClass.init(rootViewController: viewController) - } else { - viewControllerToPresent = viewController - } - - guard self.delegate?.shouldPresent(viewController: viewController, from: fromViewController) != false else { return nil } - fromViewController.present(viewControllerToPresent, animated: animated, completion: completion) - return viewController - } - - @discardableResult - public func openURL(_ url: URLConvertible, context: Any?) -> Bool { - guard let handler = self.handler(for: url, context: context) else { return false } - return handler() - } -} - - -// MARK: - Syntactic Sugars for Optional Parameters - -extension NavigatorType { - @discardableResult - public func push(_ url: URLConvertible, context: Any? = nil, from: UINavigationControllerType? = nil, animated: Bool = true) -> UIViewController? { - return self.pushURL(url, context: context, from: from, animated: animated) - } - - @discardableResult - public func push(_ viewController: UIViewController, from: UINavigationControllerType? = nil, animated: Bool = true) -> UIViewController? { - return self.pushViewController(viewController, from: from, animated: animated) - } - - @discardableResult - public func present(_ url: URLConvertible, context: Any? = nil, wrap: UINavigationController.Type? = nil, from: UIViewControllerType? = nil, animated: Bool = true, completion: (() -> Void)? = nil) -> UIViewController? { - return self.presentURL(url, context: context, wrap: wrap, from: from, animated: animated, completion: completion) - } - - @discardableResult - public func present(_ viewController: UIViewController, wrap: UINavigationController.Type? = nil, from: UIViewControllerType? = nil, animated: Bool = true, completion: (() -> Void)? = nil) -> UIViewController? { - return self.presentViewController(viewController, wrap: wrap, from: from, animated: animated, completion: completion) - } - - @discardableResult - public func open(_ url: URLConvertible, context: Any? = nil) -> Bool { - return self.openURL(url, context: context) - } -} -#endif diff --git a/Tests/URLNavigatorTests/NavigatorSpec.swift b/Tests/URLNavigatorTests/NavigatorSpec.swift index ad732f7..85b7f8e 100644 --- a/Tests/URLNavigatorTests/NavigatorSpec.swift +++ b/Tests/URLNavigatorTests/NavigatorSpec.swift @@ -9,7 +9,7 @@ import URLNavigator final class NavigatorSpec: QuickSpec { override func spec() { - var navigator: NavigatorType! + var navigator: NavigatorProtocol! beforeEach { navigator = Navigator()