1 |
//
|
|
2 |
// RouteComposer
|
|
3 |
// DefaultRouter.swift
|
|
4 |
// https://github.com/ekazaev/route-composer
|
|
5 |
//
|
|
6 |
// Created by Eugene Kazaev in 2018-2022.
|
|
7 |
// Distributed under the MIT license.
|
|
8 |
//
|
|
9 |
// Become a sponsor:
|
|
10 |
// https://github.com/sponsors/ekazaev
|
|
11 |
//
|
|
12 |
|
|
13 |
import UIKit
|
|
14 |
|
|
15 |
/// Default `Router` implementation
|
|
16 |
public struct DefaultRouter: InterceptableRouter, MainThreadChecking {
|
|
17 |
|
|
18 |
// MARK: Properties
|
|
19 |
|
|
20 |
/// `Logger` instance to be used by `DefaultRouter`.
|
|
21 |
public let logger: Logger?
|
|
22 |
|
|
23 |
/// `ContainerAdapter` instance.
|
|
24 |
public let containerAdapterLocator: ContainerAdapterLocator
|
|
25 |
|
|
26 |
/// `StackPresentationHandler` instance
|
|
27 |
public let stackPresentationHandler: StackPresentationHandler
|
|
28 |
|
|
29 |
private var interceptors: [AnyRoutingInterceptor] = []
|
45x |
30 |
|
|
31 |
private var contextTasks: [AnyContextTask] = []
|
45x |
32 |
|
|
33 |
private var postTasks: [AnyPostRoutingTask] = []
|
45x |
34 |
|
|
35 |
// MARK: Methods
|
|
36 |
|
|
37 |
/// Constructor
|
|
38 |
///
|
|
39 |
/// Parameters
|
|
40 |
/// - logger: A `Logger` instance to be used by the `DefaultRouter`.
|
|
41 |
/// - stackPresentationHandler: A `StackPresentationHandler` instance to be used by the `DefaultRouter`.
|
|
42 |
/// - containerAdapterLocator: A `ContainerAdapterLocator` instance to be used by the `DefaultRouter`.
|
|
43 |
public init(logger: Logger? = RouteComposerDefaults.shared.logger,
|
|
44 |
stackPresentationHandler: StackPresentationHandler = DefaultStackPresentationHandler(),
|
|
45 |
containerAdapterLocator: ContainerAdapterLocator = RouteComposerDefaults.shared.containerAdapterLocator) {
|
45x |
46 |
self.logger = logger
|
45x |
47 |
self.stackPresentationHandler = stackPresentationHandler
|
45x |
48 |
self.containerAdapterLocator = containerAdapterLocator
|
45x |
49 |
}
|
45x |
50 |
|
|
51 |
public mutating func add<RI: RoutingInterceptor>(_ interceptor: RI) where RI.Context == Any? {
|
15x |
52 |
interceptors.append(RoutingInterceptorBox(interceptor))
|
15x |
53 |
}
|
15x |
54 |
|
|
55 |
public mutating func add<CT: ContextTask>(_ contextTask: CT) where CT.Context == Any? {
|
2x |
56 |
contextTasks.append(ContextTaskBox(contextTask))
|
2x |
57 |
}
|
2x |
58 |
|
|
59 |
public mutating func add<PT: PostRoutingTask>(_ postTask: PT) where PT.Context == Any? {
|
2x |
60 |
postTasks.append(PostRoutingTaskBox(postTask))
|
2x |
61 |
}
|
2x |
62 |
|
|
63 |
public func navigate<Context>(to step: DestinationStep<some UIViewController, Context>,
|
|
64 |
with context: Context,
|
|
65 |
animated: Bool = true,
|
|
66 |
completion: ((_: RoutingResult) -> Void)? = nil) throws {
|
117x |
67 |
assertIfNotMainThread(logger: logger)
|
117x |
68 |
do {
|
117x |
69 |
// Wrapping real context into a box.
|
117x |
70 |
let context: AnyContext = AnyContextBox(context)
|
117x |
71 |
|
117x |
72 |
let taskStack = try prepareTaskStack(with: context)
|
117x |
73 |
let navigationStack = try prepareFactoriesStack(to: step, with: context, taskStack: taskStack)
|
117x |
74 |
|
117x |
75 |
let viewController = navigationStack.rootViewController
|
117x |
76 |
let buildingInputStack = navigationStack.buildingInputStack
|
117x |
77 |
|
117x |
78 |
// Checks if the view controllers that are currently presented from the origin view controller, can be dismissed.
|
117x |
79 |
if let viewController = Array([[viewController.allParents.last ?? viewController], viewController.allPresentedViewControllers].joined()).nonDismissibleViewController {
|
117x |
80 |
throw RoutingError.cantBeDismissed(.init("\(String(describing: viewController)) view controller cannot " +
|
4x |
81 |
"be dismissed."))
|
4x |
82 |
}
|
113x |
83 |
|
113x |
84 |
startNavigation(from: viewController,
|
113x |
85 |
building: buildingInputStack,
|
113x |
86 |
performing: taskStack,
|
113x |
87 |
animated: animated,
|
113x |
88 |
completion: { (result: RoutingResult) in
|
113x |
89 |
if case let .failure(error) = result {
|
105x |
90 |
logger?.log(.error("\(error)"))
|
4x |
91 |
logger?.log(.info("Unsuccessfully finished the navigation process."))
|
4x |
92 |
} else {
|
105x |
93 |
logger?.log(.info("Successfully finished the navigation process."))
|
101x |
94 |
}
|
105x |
95 |
completion?(result)
|
105x |
96 |
})
|
105x |
97 |
} catch {
|
113x |
98 |
logger?.log(.error("\(error)"))
|
12x |
99 |
logger?.log(.info("Unsuccessfully finished the navigation process."))
|
12x |
100 |
throw error
|
12x |
101 |
}
|
117x |
102 |
}
|
117x |
103 |
|
|
104 |
// MARK: Private Methods
|
|
105 |
|
|
106 |
private func prepareTaskStack(with context: AnyContext) throws -> GlobalTaskRunner {
|
117x |
107 |
let interceptorRunner = try InterceptorRunner(interceptors: interceptors, with: context)
|
117x |
108 |
let contextTaskRunner = try ContextTaskRunner(contextTasks: contextTasks, with: context)
|
117x |
109 |
let postponedTaskRunner = PostponedTaskRunner()
|
117x |
110 |
let postTaskRunner = PostTaskRunner(postTasks: postTasks, postponedRunner: postponedTaskRunner)
|
117x |
111 |
return GlobalTaskRunner(interceptorRunner: interceptorRunner, contextTaskRunner: contextTaskRunner, postTaskRunner: postTaskRunner)
|
117x |
112 |
}
|
117x |
113 |
|
|
114 |
private func prepareFactoriesStack(to finalStep: RoutingStep, with context: AnyContext, taskStack: GlobalTaskRunner) throws -> (rootViewController: UIViewController,
|
|
115 |
buildingInputStack: [(factory: AnyFactory, context: AnyContext)]) {
|
117x |
116 |
logger?.log(.info("Started to search for the view controller to start the navigation process from."))
|
117x |
117 |
|
117x |
118 |
var context = context
|
117x |
119 |
|
117x |
120 |
let stepSequence = sequence(first: finalStep, next: { ($0 as? ChainableStep)?.getPreviousStep(with: context) }).compactMap { $0 as? PerformableStep }
|
714x |
121 |
|
117x |
122 |
let result = try stepSequence.reduce((rootViewController: UIViewController?, buildingInputStack: [(factory: AnyFactory, context: AnyContext)])(rootViewController: nil, buildingInputStack: [])) { result, step in
|
378x |
123 |
guard result.rootViewController == nil else {
|
378x |
124 |
return result
|
108x |
125 |
}
|
270x |
126 |
|
270x |
127 |
// Creates a class responsible to run the tasks for this particular step
|
270x |
128 |
let stepTaskRunner = try taskStack.taskRunner(for: step, with: context)
|
270x |
129 |
|
270x |
130 |
switch try step.perform(with: context) {
|
270x |
131 |
case let .success(viewController):
|
270x |
132 |
logger?.log(.info("\(String(describing: step)) found " +
|
109x |
133 |
"\(String(describing: viewController)) to start the navigation process from."))
|
109x |
134 |
|
109x |
135 |
try stepTaskRunner.perform(on: viewController)
|
109x |
136 |
|
109x |
137 |
return (rootViewController: viewController, result.buildingInputStack)
|
109x |
138 |
case let .build(originalFactory):
|
270x |
139 |
logger?.log(.info("\(String(describing: step)) hasn't found a corresponding view " +
|
134x |
140 |
"controller in the stack, so it will be built using \(String(describing: originalFactory))."))
|
134x |
141 |
|
134x |
142 |
// Wrap the `Factory` with the decorator that will
|
134x |
143 |
// handle the view controller and post task chain after the view controller creation.
|
134x |
144 |
var factory = FactoryDecorator(factory: originalFactory, stepTaskRunner: stepTaskRunner)
|
134x |
145 |
|
134x |
146 |
// Prepares the `Factory` for integration
|
134x |
147 |
// If a `Factory` cannot prepare itself (e.g. does not have enough data in context)
|
134x |
148 |
// then the view controllers stack can not be built
|
134x |
149 |
try factory.prepare(with: context)
|
134x |
150 |
|
134x |
151 |
// Allows to the `Factory` to change the current factory stack if needed.
|
134x |
152 |
var buildingInputStack = try factory.scrapeChildren(from: result.buildingInputStack)
|
134x |
153 |
|
134x |
154 |
// Adds the `Factory` to the beginning of the stack as the router is reading the configuration backwards.
|
134x |
155 |
buildingInputStack.insert((factory: factory, context: context), at: 0)
|
134x |
156 |
return (rootViewController: result.rootViewController, buildingInputStack: buildingInputStack)
|
134x |
157 |
case let .updateContext(newContext):
|
270x |
158 |
// Substitute current context with an updated one
|
11x |
159 |
context = newContext
|
11x |
160 |
return result
|
11x |
161 |
case .none:
|
270x |
162 |
logger?.log(.info("\(String(describing: step)) hasn't found a corresponding view " +
|
9x |
163 |
"controller in the stack, so router will continue to search."))
|
9x |
164 |
return result
|
9x |
165 |
}
|
270x |
166 |
}
|
269x |
167 |
|
117x |
168 |
// Throw an exception if the router hasn't found a view controller to start the stack from.
|
117x |
169 |
guard let rootViewController = result.rootViewController else {
|
117x |
170 |
throw RoutingError.initialController(.notFound, .init("Unable to start the navigation process as the view controller to start from was not found."))
|
1x |
171 |
}
|
116x |
172 |
|
116x |
173 |
return (rootViewController: rootViewController, buildingInputStack: result.buildingInputStack)
|
116x |
174 |
}
|
117x |
175 |
|
|
176 |
private func startNavigation(from viewController: UIViewController,
|
|
177 |
building buildingInputStack: [(factory: AnyFactory, context: AnyContext)],
|
|
178 |
performing taskStack: GlobalTaskRunner,
|
|
179 |
animated: Bool,
|
|
180 |
completion: @escaping (RoutingResult) -> Void) {
|
105x |
181 |
// Executes interceptors associated to each view in the chain. All the interceptors must succeed to
|
105x |
182 |
// continue navigation process. This operation is async.
|
105x |
183 |
let initialControllerDescription = String(describing: viewController)
|
105x |
184 |
taskStack.performInterceptors { [weak viewController] result in
|
105x |
185 |
self.assertIfNotMainThread(logger: logger)
|
105x |
186 |
|
105x |
187 |
if case let .failure(error) = result {
|
105x |
188 |
completion(.failure(error))
|
1x |
189 |
return
|
1x |
190 |
}
|
104x |
191 |
|
104x |
192 |
guard let viewController else {
|
104x |
193 |
completion(.failure(RoutingError.initialController(.deallocated, .init("A view controller \(initialControllerDescription) that has been chosen as a " +
|
1x |
194 |
"starting point of the navigation process was destroyed while the router was waiting for the interceptors to finish."))))
|
1x |
195 |
return
|
1x |
196 |
}
|
103x |
197 |
|
103x |
198 |
// Closes all the presented view controllers above the found view controller to be able
|
103x |
199 |
// to build a new stack if needed.
|
103x |
200 |
// This operation is async.
|
103x |
201 |
// It was already confirmed that they can be dismissed.
|
103x |
202 |
stackPresentationHandler.dismissPresented(from: viewController, animated: animated) { result in
|
103x |
203 |
guard result.isSuccessful else {
|
103x |
204 |
completion(result)
|
! |
205 |
return
|
! |
206 |
}
|
103x |
207 |
|
103x |
208 |
// Builds view controller's stack using factories.
|
103x |
209 |
// This operation is async.
|
103x |
210 |
buildViewControllerStack(starting: viewController,
|
103x |
211 |
using: buildingInputStack,
|
103x |
212 |
animated: animated) { result in
|
103x |
213 |
do {
|
103x |
214 |
if case let .failure(error) = result {
|
103x |
215 |
throw error
|
2x |
216 |
}
|
101x |
217 |
try taskStack.performPostTasks()
|
101x |
218 |
completion(result)
|
101x |
219 |
} catch {
|
101x |
220 |
completion(.failure(error))
|
2x |
221 |
}
|
103x |
222 |
}
|
103x |
223 |
}
|
103x |
224 |
}
|
103x |
225 |
}
|
105x |
226 |
|
|
227 |
// Loops through the list of factories and builds their view controllers in sequence.
|
|
228 |
// Some actions can be asynchronous, like push, modal or presentations,
|
|
229 |
// so it performs them asynchronously
|
|
230 |
private func buildViewControllerStack(starting rootViewController: UIViewController,
|
|
231 |
using factories: [(factory: AnyFactory, context: AnyContext)],
|
|
232 |
animated: Bool,
|
|
233 |
completion: @escaping (RoutingResult) -> Void) {
|
103x |
234 |
var factories = factories
|
103x |
235 |
let postponedIntegrationHandler = DefaultPostponedIntegrationHandler(logger: logger,
|
103x |
236 |
containerAdapterLocator: containerAdapterLocator)
|
103x |
237 |
|
103x |
238 |
func buildViewController(from previousViewController: UIViewController) {
|
198x |
239 |
stackPresentationHandler.makeVisibleInParentContainers(previousViewController, animated: animated) { result in
|
198x |
240 |
guard result.isSuccessful else {
|
198x |
241 |
logger?.log(.info("\(String(describing: previousViewController)) has stopped the navigation process " +
|
! |
242 |
"as it was not able to become active."))
|
! |
243 |
completion(result)
|
! |
244 |
return
|
! |
245 |
}
|
198x |
246 |
guard !factories.isEmpty else {
|
198x |
247 |
postponedIntegrationHandler.purge(animated: animated, completion: completion)
|
101x |
248 |
return
|
101x |
249 |
}
|
101x |
250 |
do {
|
97x |
251 |
let factory = factories.removeFirst()
|
97x |
252 |
let newViewController = try factory.factory.build(with: factory.context)
|
97x |
253 |
logger?.log(.info("\(String(describing: factory)) built a \(String(describing: newViewController))."))
|
97x |
254 |
|
97x |
255 |
let nextAction = factories.first?.factory.action
|
97x |
256 |
|
97x |
257 |
factory.factory.action.perform(with: newViewController,
|
97x |
258 |
on: previousViewController,
|
97x |
259 |
with: postponedIntegrationHandler,
|
97x |
260 |
nextAction: nextAction,
|
97x |
261 |
animated: animated) { result in
|
97x |
262 |
self.assertIfNotMainThread(logger: logger)
|
96x |
263 |
guard result.isSuccessful else {
|
96x |
264 |
logger?.log(.info("\(String(describing: factory.factory.action)) has stopped the navigation process " +
|
1x |
265 |
"as it was not able to build a view controller into a stack."))
|
1x |
266 |
completion(result)
|
1x |
267 |
return
|
1x |
268 |
}
|
95x |
269 |
logger?.log(.info("\(String(describing: factory.factory.action)) has applied to " +
|
95x |
270 |
"\(String(describing: previousViewController)) with \(String(describing: newViewController))."))
|
95x |
271 |
buildViewController(from: newViewController)
|
95x |
272 |
}
|
95x |
273 |
} catch {
|
97x |
274 |
completion(.failure(error))
|
1x |
275 |
}
|
97x |
276 |
}
|
97x |
277 |
}
|
198x |
278 |
|
103x |
279 |
logger?.log(.info(factories.isEmpty ? "No view controllers needed to be integrated into the stack." : "Started to build the view controllers stack."))
|
103x |
280 |
buildViewController(from: rootViewController)
|
103x |
281 |
}
|
103x |
282 |
|
|
283 |
}
|
|