Slather logo

Coverage for "DefaultRouter.swift" : 97.22%

(210 of 216 relevant lines covered)

RouteComposer/Classes/Router/DefaultRouter.swift

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
}