Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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/<int:id>") { ... }
navigator.register("myapp://post/<title>") { ... }
navigator.handle("myapp://alert") { ... }
Expand Down
107 changes: 105 additions & 2 deletions Sources/URLNavigator/Navigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,140 @@ 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 }
guard let factory = self.viewControllerFactories[match.pattern] else { return nil }
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
60 changes: 60 additions & 0 deletions Sources/URLNavigator/NavigatorProtocol.swift
Original file line number Diff line number Diff line change
@@ -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
163 changes: 0 additions & 163 deletions Sources/URLNavigator/NavigatorType.swift

This file was deleted.

2 changes: 1 addition & 1 deletion Tests/URLNavigatorTests/NavigatorSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import URLNavigator

final class NavigatorSpec: QuickSpec {
override func spec() {
var navigator: NavigatorType!
var navigator: NavigatorProtocol!

beforeEach {
navigator = Navigator()
Expand Down