-
Notifications
You must be signed in to change notification settings - Fork 79
Expand file tree
/
Copy pathRouter.swift
More file actions
223 lines (179 loc) · 9.35 KB
/
Router.swift
File metadata and controls
223 lines (179 loc) · 9.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
//
// Router.swift
// Meet
//
// Created by Benjamin Encz on 11/11/15.
// Copyright © 2015 DigiTales. All rights reserved.
//
import Foundation
import ReSwift
open class Router<State: StateType>: StoreSubscriber {
public typealias NavigationStateTransform = (Subscription<State>) -> Subscription<NavigationState>
var store: Store<State>
var lastNavigationState = NavigationState()
var routables: [Routable] = []
let waitForRoutingCompletionQueue = DispatchQueue(label: "WaitForRoutingCompletionQueue", attributes: [])
public init(store: Store<State>, rootRoutable: Routable, stateTransform: @escaping NavigationStateTransform) {
self.store = store
self.routables.append(rootRoutable)
self.store.subscribe(self, transform: stateTransform)
}
open func newState(state: NavigationState) {
let routingActions = Router.routingActionsForTransition(from: lastNavigationState.route,
to: state.route)
routingActions.forEach { routingAction in
let semaphore = DispatchSemaphore(value: 0)
// Dispatch all routing actions onto this dedicated queue. This will ensure that
// only one routing action can run at any given time. This is important for using this
// Router with UI frameworks. Whenever a navigation action is triggered, this queue will
// block (using semaphore_wait) until it receives a callback from the Routable
// indicating that the navigation action has completed
waitForRoutingCompletionQueue.async {
switch routingAction {
case let .pop(responsibleRoutableIndex, elementToBePopped):
DispatchQueue.main.async {
if !state.disablePopAction {
self.routables[responsibleRoutableIndex]
.pop(
elementToBePopped,
animated: state.changeRouteAnimated) {
semaphore.signal()
}
} else {
semaphore.signal()
}
self.routables.remove(at: responsibleRoutableIndex + 1)
}
case let .change(responsibleRoutableIndex, elementToBeReplaced, newElement):
DispatchQueue.main.async {
self.routables[responsibleRoutableIndex + 1] =
self.routables[responsibleRoutableIndex]
.change(
elementToBeReplaced,
to: newElement,
animated: state.changeRouteAnimated) {
semaphore.signal()
}
}
case let .push(responsibleRoutableIndex, elementToBePushed):
DispatchQueue.main.async {
self.routables.append(
self.routables[responsibleRoutableIndex]
.push(
elementToBePushed,
animated: state.changeRouteAnimated) {
semaphore.signal()
}
)
}
}
let waitUntil = DispatchTime.now() + Double(Int64(3 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
let result = semaphore.wait(timeout: waitUntil)
if case .timedOut = result {
print("[ReSwiftRouter]: Router is stuck waiting for a" +
" completion handler to be called. Ensure that you have called the" +
" completion handler in each Routable element.")
print("Set a symbolic breakpoint for the `ReSwiftRouterStuck` symbol in order" +
" to halt the program when this happens")
ReSwiftRouterStuck()
}
}
}
lastNavigationState = state
if (state.disablePopAction) {
store.dispatch(EnablePopAction())
}
}
// MARK: Route Transformation Logic
static func largestCommonSubroute(_ oldRoute: Route, newRoute: Route) -> Int {
var largestCommonSubroute = -1
while largestCommonSubroute + 1 < newRoute.count &&
largestCommonSubroute + 1 < oldRoute.count &&
newRoute[largestCommonSubroute + 1] == oldRoute[largestCommonSubroute + 1] {
largestCommonSubroute += 1
}
return largestCommonSubroute
}
// Maps Route index to Routable index. Routable index is offset by 1 because the root Routable
// is not represented in the route, e.g.
// route = ["tabBar"]
// routables = [RootRoutable, TabBarRoutable]
static func routableIndex(for element: Int) -> Int {
return element + 1
}
static func routingActionsForTransition(
from oldRoute: Route,
to newRoute: Route) -> [RoutingActions] {
var routingActions: [RoutingActions] = []
// Find the last common subroute between two routes
let commonSubroute = largestCommonSubroute(oldRoute, newRoute: newRoute)
if commonSubroute == oldRoute.count - 1 && commonSubroute == newRoute.count - 1 {
return []
}
// Keeps track which element of the routes we are working on
// We start at the end of the old route
var routeBuildingIndex = oldRoute.count - 1
// 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 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 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 routeElementToPop = oldRoute[routeBuildingIndex]
let popAction = RoutingActions.pop(
responsibleRoutableIndex: routableIndex(for: routeBuildingIndex - 1),
elementToBePopped: routeElementToPop
)
routingActions.append(popAction)
routeBuildingIndex -= 1
}
// This is the 3. case:
// "The new route has a different element after the commonSubroute, we need to replace
// the old route element with the new one"
if oldRoute.count > (commonSubroute + 1) && newRoute.count > (commonSubroute + 1) {
let changeAction = RoutingActions.change(
responsibleRoutableIndex: routableIndex(for: commonSubroute),
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 element after the commonSubroute"
else if oldRoute.count > newRoute.count {
let popAction = RoutingActions.pop(
responsibleRoutableIndex: routableIndex(for: routeBuildingIndex - 1),
elementToBePopped: oldRoute[routeBuildingIndex]
)
routingActions.append(popAction)
routeBuildingIndex -= 1
}
// 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 element(s) after the commonSubroute"
let newRouteIndex = newRoute.count - 1
while routeBuildingIndex < newRouteIndex {
let routeElementToPush = newRoute[routeBuildingIndex + 1]
let pushAction = RoutingActions.push(
responsibleRoutableIndex: routableIndex(for: routeBuildingIndex),
elementToBePushed: routeElementToPush
)
routingActions.append(pushAction)
routeBuildingIndex += 1
}
return routingActions
}
}
func ReSwiftRouterStuck() {}
enum RoutingActions {
case push(responsibleRoutableIndex: Int, elementToBePushed: RouteElement)
case pop(responsibleRoutableIndex: Int, elementToBePopped: RouteElement)
case change(responsibleRoutableIndex: Int, elementToBeReplaced: RouteElement,
newElement: RouteElement)
}