Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
60 changes: 33 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,66 +84,72 @@ This will make reducer handle all routing relevant actions.

# Implementing `Routable`

ReSwiftRouter works with routes that are defined, similar to URLs, as a sequence of identifiers e.g. `["Home", "User", "UserDetail"]`.
ReSwiftRouter works with routes that are defined, similar to URLs, as a sequence of elements e.g. `["Home", "User", "UserDetail"]`.

ReSwiftRouter is agnostic of the UI framework you are using - it uses `Routable`s to implement that interaction.

Each route segment is mapped to one responsible `Routable`. The `Routable` needs to be able to present a child, hide a child or replace a child with another child.
Each route element is mapped to one responsible `Routable`. The `Routable` needs to be able to present a child, hide a child or replace a child with another child.

Here is the `Routable` protocol with the methods you should implement:

```swift
protocol Routable {

func changeRouteSegment(from: RouteElementIdentifier,
to: RouteElementIdentifier,
completionHandler: RoutingCompletionHandler) -> Routable
public protocol Routable {

func pushRouteSegment(routeElementIdentifier: RouteElementIdentifier,
completionHandler: RoutingCompletionHandler) -> Routable
func push(
_ element: RouteElement,
animated: Bool,
completion: @escaping RoutingCompletion) -> Routable

func popRouteSegment(routeElementIdentifier: RouteElementIdentifier,
completionHandler: RoutingCompletionHandler)
func pop(
_ element: RouteElement,
animated: Bool,
completion: @escaping RoutingCompletion)

func change(
_ from: RouteElement,
to: RouteElement,
animated: Bool,
completion: @escaping RoutingCompletion) -> Routable

}

```

As part of initializing `Router` you need to pass the first `Routable` as an argument. That root `Routable` will be responsible for the first route segment.
As part of initializing `Router` you need to pass the first `Routable` as an argument. That root `Routable` will be responsible for the first route element.

If e.g. you set the route of your application to `["Home"]`, your root `Routable` will be asked to present the view that corresponds to the identifier `"Home"`.
If e.g. you set the route of your application to `["Home"]`, your root `Routable` will be asked to present the view that corresponds to the element `"Home"`.

When working on iOS with UIKit this would mean the `Routable` would need to set the `rootViewController` of the application.

Whenever a `Routable` presents a new route segment, it needs to return a new `Routable` that will be responsible for managing the presented segment. If you want to navigate from `["Home"]` to `["Home", "Users"]` the `Routable` responsible for the `"Home"` segment will be asked to present the `"User"` segment.
Whenever a `Routable` presents a new route element, it needs to return a new `Routable` that will be responsible for managing the presented element. If you want to navigate from `["Home"]` to `["Home", "Users"]` the `Routable` responsible for the `"Home"` element will be asked to present the `"User"` element.

If your navigation stack uses a modal presentation for this transition, the implementation of `Routable` for the `"Home"` segment might look like this:
If your navigation stack uses a modal presentation for this transition, the implementation of `Routable` for the `"Home"` element might look like this:

```swift
func pushRouteSegment(identifier: RouteElementIdentifier,
completionHandler: RoutingCompletionHandler) -> Routable {

if identifier == "User" {
func push(_ element: RouteElement, animated: Bool, completion: @escaping RoutingCompletion) -> Routable {

if element == "User" {
// 1.) Perform the transition
userViewController = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewControllerWithIdentifier("UserViewController") as! Routable

// 2.) Call the `completionHandler` once the transition is complete
// 2.) Call the `completion` once the transition is complete
presentViewController(userViewController, animated: false,
completion: completionHandler)
completion: completion)

// 3.) Return the Routable for the presented segment. For convenience
// 3.) Return the Routable for the presented element. For convenience
// this will often be the UIViewController itself.
return userViewController
}

// ...
}

func popRouteSegment(identifier: RouteElementIdentifier,
completionHandler: RoutingCompletionHandler) {
func pop(_ element: RouteElement, animated: Bool, completion: @escaping RoutingCompletion)

if identifier == "Home" {
dismissViewControllerAnimated(false, completion: completionHandler)
if element == "Home" {
dismissViewControllerAnimated(false, completion: completion)
}

// ...
Expand All @@ -152,7 +158,7 @@ func popRouteSegment(identifier: RouteElementIdentifier,

## Calling the Completion Handler within Routables

ReSwiftRouter needs to throttle the navigation actions, since many UI frameworks including UIKit don't allow to perform multiple navigation steps in parallel. Therefor every method of `Routable` receives a `completionHandler`. The router will not perform any further navigation actions until the completion handler is called.
ReSwiftRouter needs to throttle the navigation actions, since many UI frameworks including UIKit don't allow to perform multiple navigation steps in parallel. Therefor every method of `Routable` receives a `completion` handler. The router will not perform any further navigation actions until the completion handler is called.

# Changing the Current Route

Expand All @@ -165,7 +171,7 @@ Currently the only way to change the current application route is by using the `
)
}
```
As development continues, support for changing individual route segments will be added.
As development continues, support for changing individual route elements will be added.


# Contributing
Expand Down
4 changes: 2 additions & 2 deletions ReSwiftRouter/NavigationState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

import ReSwift

public typealias RouteElementIdentifier = String
public typealias Route = [RouteElementIdentifier]
public typealias RouteElement = String
public typealias Route = [RouteElement]

/// A `Hashable` and `Equatable` presentation of a route.
/// Can be used to check two routes for equality.
Expand Down
40 changes: 10 additions & 30 deletions ReSwiftRouter/Routable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,30 @@
// Copyright © 2015 DigiTales. All rights reserved.
//

public typealias RoutingCompletionHandler = () -> Void
public typealias RoutingCompletion = () -> Void

public protocol Routable {

func pushRouteSegment(
_ routeElementIdentifier: RouteElementIdentifier,
animated: Bool,
completionHandler: @escaping RoutingCompletionHandler) -> Routable
func push(_ element: RouteElement, animated: Bool, completion: @escaping RoutingCompletion) -> Routable

func popRouteSegment(
_ routeElementIdentifier: RouteElementIdentifier,
animated: Bool,
completionHandler: @escaping RoutingCompletionHandler)
func pop(_ element: RouteElement, animated: Bool, completion: @escaping RoutingCompletion)

func changeRouteSegment(
_ from: RouteElementIdentifier,
to: RouteElementIdentifier,
animated: Bool,
completionHandler: @escaping RoutingCompletionHandler) -> Routable
func change(_ from: RouteElement, to: RouteElement, animated: Bool, completion: @escaping RoutingCompletion) -> Routable

}

extension Routable {

public func pushRouteSegment(
_ routeElementIdentifier: RouteElementIdentifier,
animated: Bool,
completionHandler: @escaping RoutingCompletionHandler) -> Routable {
fatalError("This routable cannot push segments. You have not implemented it. (Asked \(type(of: self)) to push \(routeElementIdentifier))")
public func push(_ element: RouteElement, animated: Bool, completion: @escaping RoutingCompletion) -> Routable {
fatalError("This routable cannot push elements. You have not implemented it. (Asked \(type(of: self)) to push \(element))")
}

public func popRouteSegment(
_ routeElementIdentifier: RouteElementIdentifier,
animated: Bool,
completionHandler: @escaping RoutingCompletionHandler) {
fatalError("This routable cannot pop segments. You have not implemented it. (Asked \(type(of: self)) to pop \(routeElementIdentifier))")
public func pop(_ element: RouteElement, animated: Bool, completion: @escaping RoutingCompletion) {
fatalError("This routable cannot pop elements. You have not implemented it. (Asked \(type(of: self)) to pop \(element))")
}

public func changeRouteSegment(
_ from: RouteElementIdentifier,
to: RouteElementIdentifier,
animated: Bool,
completionHandler: @escaping RoutingCompletionHandler) -> Routable {
fatalError("This routable cannot change segments. You have not implemented it. (Asked \(type(of: self)) to change from \(from) to \(to))")
public func change(_ from: RouteElement, to: RouteElement, animated: Bool, completion: @escaping RoutingCompletion) -> Routable {
fatalError("This routable cannot change elements. You have not implemented it. (Asked \(type(of: self)) to change from \(from) to \(to))")
}

}
56 changes: 28 additions & 28 deletions ReSwiftRouter/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,36 +40,36 @@ open class Router<State: StateType>: StoreSubscriber {
waitForRoutingCompletionQueue.async {
switch routingAction {

case let .pop(responsibleRoutableIndex, segmentToBePopped):
case let .pop(responsibleRoutableIndex, elementToBePopped):
DispatchQueue.main.async {
self.routables[responsibleRoutableIndex]
.popRouteSegment(
segmentToBePopped,
.pop(
elementToBePopped,
animated: state.changeRouteAnimated) {
semaphore.signal()
}

self.routables.remove(at: responsibleRoutableIndex + 1)
}

case let .change(responsibleRoutableIndex, segmentToBeReplaced, newSegment):
case let .change(responsibleRoutableIndex, elementToBeReplaced, newElement):
DispatchQueue.main.async {
self.routables[responsibleRoutableIndex + 1] =
self.routables[responsibleRoutableIndex]
.changeRouteSegment(
segmentToBeReplaced,
to: newSegment,
.change(
elementToBeReplaced,
to: newElement,
animated: state.changeRouteAnimated) {
semaphore.signal()
}
}

case let .push(responsibleRoutableIndex, segmentToBePushed):
case let .push(responsibleRoutableIndex, elementToBePushed):
DispatchQueue.main.async {
self.routables.append(
self.routables[responsibleRoutableIndex]
.pushRouteSegment(
segmentToBePushed,
.push(
elementToBePushed,
animated: state.changeRouteAnimated) {
semaphore.signal()
}
Expand Down Expand Up @@ -114,8 +114,8 @@ open class Router<State: StateType>: StoreSubscriber {
// is not represented in the route, e.g.
// route = ["tabBar"]
// routables = [RootRoutable, TabBarRoutable]
static func routableIndex(for segment: Int) -> Int {
return segment + 1
static func routableIndex(for element: Int) -> Int {
return element + 1
}

static func routingActionsForTransition(
Expand All @@ -134,22 +134,22 @@ open class Router<State: StateType>: StoreSubscriber {
// We start at the end of the old route
var routeBuildingIndex = oldRoute.count - 1

// Pop all route segments of the old route that are no longer in the new route
// Pop all route elements of the old route that are no longer in the new route
// Stop one element ahead of the commonSubroute. When we are one element ahead of the
// commmon subroute we have three options:
//
// 1. The old route had an element after the commonSubroute and the new route does not
// we need to pop the route segment after the commonSubroute
// we need to pop the route element after the commonSubroute
// 2. The old route had no element after the commonSubroute and the new route does, we
// we need to push the route segment(s) after the commonSubroute
// we need to push the route element(s) after the commonSubroute
// 3. The new route has a different element after the commonSubroute, we need to replace
// the old route element with the new one
while routeBuildingIndex > commonSubroute + 1 {
let routeSegmentToPop = oldRoute[routeBuildingIndex]
let routeElementToPop = oldRoute[routeBuildingIndex]

let popAction = RoutingActions.pop(
responsibleRoutableIndex: routableIndex(for: routeBuildingIndex - 1),
segmentToBePopped: routeSegmentToPop
elementToBePopped: routeElementToPop
)

routingActions.append(popAction)
Expand All @@ -162,18 +162,18 @@ open class Router<State: StateType>: StoreSubscriber {
if oldRoute.count > (commonSubroute + 1) && newRoute.count > (commonSubroute + 1) {
let changeAction = RoutingActions.change(
responsibleRoutableIndex: routableIndex(for: commonSubroute),
segmentToBeReplaced: oldRoute[commonSubroute + 1],
newSegment: newRoute[commonSubroute + 1])
elementToBeReplaced: oldRoute[commonSubroute + 1],
newElement: newRoute[commonSubroute + 1])

routingActions.append(changeAction)
}
// This is the 1. case:
// "The old route had an element after the commonSubroute and the new route does not
// we need to pop the route segment after the commonSubroute"
// we need to pop the route element after the commonSubroute"
else if oldRoute.count > newRoute.count {
let popAction = RoutingActions.pop(
responsibleRoutableIndex: routableIndex(for: routeBuildingIndex - 1),
segmentToBePopped: oldRoute[routeBuildingIndex]
elementToBePopped: oldRoute[routeBuildingIndex]
)

routingActions.append(popAction)
Expand All @@ -184,15 +184,15 @@ open class Router<State: StateType>: StoreSubscriber {
// Push remainder of elements in new Route that weren't in old Route, this covers
// the 2. case:
// "The old route had no element after the commonSubroute and the new route does,
// we need to push the route segment(s) after the commonSubroute"
// we need to push the route element(s) after the commonSubroute"
let newRouteIndex = newRoute.count - 1

while routeBuildingIndex < newRouteIndex {
let routeSegmentToPush = newRoute[routeBuildingIndex + 1]
let routeElementToPush = newRoute[routeBuildingIndex + 1]

let pushAction = RoutingActions.push(
responsibleRoutableIndex: routableIndex(for: routeBuildingIndex),
segmentToBePushed: routeSegmentToPush
elementToBePushed: routeElementToPush
)

routingActions.append(pushAction)
Expand All @@ -207,8 +207,8 @@ open class Router<State: StateType>: StoreSubscriber {
func ReSwiftRouterStuck() {}

enum RoutingActions {
case push(responsibleRoutableIndex: Int, segmentToBePushed: RouteElementIdentifier)
case pop(responsibleRoutableIndex: Int, segmentToBePopped: RouteElementIdentifier)
case change(responsibleRoutableIndex: Int, segmentToBeReplaced: RouteElementIdentifier,
newSegment: RouteElementIdentifier)
case push(responsibleRoutableIndex: Int, elementToBePushed: RouteElement)
case pop(responsibleRoutableIndex: Int, elementToBePopped: RouteElement)
case change(responsibleRoutableIndex: Int, elementToBeReplaced: RouteElement,
newElement: RouteElement)
}
Loading