| 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 |
}
|
|