Slather logo

Coverage for "DefaultRouter+Extension.swift" : 92.06%

(197 of 214 relevant lines covered)

RouteComposer/Classes/Router/Internal/DefaultRouter+Extension.swift

1
//
2
// RouteComposer
3
// DefaultRouter+Extension.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 Foundation
14
import UIKit
15
16
extension DefaultRouter {
17
18
    struct InterceptorRunner {
19
20
        private var interceptors: [(interceptor: AnyRoutingInterceptor, context: AnyContext)]
21
22
        init(interceptors: [AnyRoutingInterceptor], with context: AnyContext) throws {
217x
23
            self.interceptors = try interceptors.map {
302x
24
                var interceptor = $0
302x
25
                try interceptor.prepare(with: context)
302x
26
                return (interceptor: interceptor, context: context)
302x
27
            }
302x
28
        }
217x
29
30
        mutating func add(_ interceptor: AnyRoutingInterceptor, with context: AnyContext) throws {
27x
31
            var interceptor = interceptor
27x
32
            try interceptor.prepare(with: context)
27x
33
            interceptors.append((interceptor: interceptor, context: context))
27x
34
        }
27x
35
36
        func perform(completion: @escaping (_: RoutingResult) -> Void) {
205x
37
            guard !interceptors.isEmpty else {
205x
38
                completion(.success)
5x
39
                return
5x
40
            }
200x
41
200x
42
            var interceptors = interceptors
200x
43
200x
44
            func runInterceptor(interceptor: (interceptor: AnyRoutingInterceptor, context: AnyContext)) {
325x
45
                interceptor.interceptor.perform(with: interceptor.context) { result in
325x
46
                    if case .failure = result {
325x
47
                        completion(result)
1x
48
                    } else if interceptors.isEmpty {
325x
49
                        completion(result)
199x
50
                    } else {
325x
51
                        runInterceptor(interceptor: interceptors.removeFirst())
126x
52
                    }
325x
53
                }
325x
54
            }
325x
55
200x
56
            runInterceptor(interceptor: interceptors.removeFirst())
200x
57
        }
200x
58
59
    }
60
61
    struct ContextTaskRunner {
62
63
        var contextTasks: [AnyContextTask]
64
65
        init(contextTasks: [AnyContextTask], with context: AnyContext) throws {
117x
66
            self.contextTasks = try contextTasks.map {
117x
67
                var contextTask = $0
2x
68
                try contextTask.prepare(with: context)
2x
69
                return contextTask
2x
70
            }
2x
71
        }
117x
72
73
        mutating func add(_ contextTask: AnyContextTask, with context: AnyContext) throws {
106x
74
            var contextTask = contextTask
106x
75
            try contextTask.prepare(with: context)
106x
76
            contextTasks.append(contextTask)
106x
77
        }
106x
78
79
        func perform(on viewController: UIViewController, with context: AnyContext) throws {
236x
80
            try contextTasks.forEach {
236x
81
                try $0.perform(on: viewController, with: context)
107x
82
            }
107x
83
        }
236x
84
85
    }
86
87
    struct PostTaskRunner {
88
89
        var postTasks: [AnyPostRoutingTask]
90
91
        let postponedRunner: PostponedTaskRunner
92
93
        init(postTasks: [AnyPostRoutingTask], postponedRunner: PostponedTaskRunner) {
117x
94
            self.postTasks = postTasks
117x
95
            self.postponedRunner = postponedRunner
117x
96
        }
117x
97
98
        mutating func add(_ postTask: AnyPostRoutingTask) throws {
13x
99
            postTasks.append(postTask)
13x
100
        }
13x
101
102
        func perform(on viewController: UIViewController, with context: AnyContext) throws {
236x
103
            postponedRunner.add(postTasks: postTasks, to: viewController, context: context)
236x
104
        }
236x
105
106
        func commit() throws {
101x
107
            try postponedRunner.perform()
101x
108
        }
101x
109
110
    }
111
112
    struct StepTaskTaskRunner {
113
114
        private let contextTaskRunner: ContextTaskRunner
115
116
        private let postTaskRunner: PostTaskRunner
117
118
        private let context: AnyContext
119
120
        init(contextTaskRunner: ContextTaskRunner, postTaskRunner: PostTaskRunner, context: AnyContext) {
269x
121
            self.contextTaskRunner = contextTaskRunner
269x
122
            self.postTaskRunner = postTaskRunner
269x
123
            self.context = context
269x
124
        }
269x
125
126
        func perform(on viewController: UIViewController) throws {
236x
127
            try contextTaskRunner.perform(on: viewController, with: context)
236x
128
            try postTaskRunner.perform(on: viewController, with: context)
236x
129
        }
236x
130
131
    }
132
133
    final class PostponedTaskRunner {
134
135
        private struct PostTaskSlip {
136
            // This reference is weak because even though this view controller was created by a fabric but then some other
137
            // view controller in the chain can have an action that will actually remove this view controller from the
138
            // stack. We do not want to keep a strong reference to it and prevent it from deallocation. Potentially it's
139
            // a very rare issue but must be kept in mind.
140
            weak var viewController: UIViewController?
141
142
            let postTask: AnyPostRoutingTask
143
        }
144
145
        // this class is just a placeholder. Router needs at least one post routing task per view controller to
146
        // store a reference there.
147
        private struct EmptyPostTask: AnyPostRoutingTask {
148
149
            func perform(on viewController: UIViewController, with context: AnyContext, routingStack: [UIViewController]) {}
210x
150
151
        }
152
153
        private final var taskSlips: [(postTaskSlip: PostTaskSlip, context: AnyContext)] = []
118x
154
155
        final func add(postTasks: [AnyPostRoutingTask], to viewController: UIViewController, context: AnyContext) {
238x
156
            guard !postTasks.isEmpty else {
238x
157
                let postTaskSlip = PostTaskSlip(viewController: viewController, postTask: EmptyPostTask())
219x
158
                taskSlips.append((postTaskSlip: postTaskSlip, context: context))
219x
159
                return
219x
160
            }
219x
161
19x
162
            postTasks.forEach {
21x
163
                let postTaskSlip = PostTaskSlip(viewController: viewController, postTask: $0)
21x
164
                taskSlips.append((postTaskSlip: postTaskSlip, context: context))
21x
165
            }
21x
166
        }
19x
167
168
        final func perform() throws {
103x
169
            var viewControllers: [UIViewController] = []
103x
170
            taskSlips.forEach {
232x
171
                guard let viewController = $0.postTaskSlip.viewController, !viewControllers.contains(viewController) else {
232x
172
                    return
7x
173
                }
225x
174
                viewControllers.append(viewController)
225x
175
            }
225x
176
103x
177
            try taskSlips.forEach { slip in
232x
178
                guard let viewController = slip.postTaskSlip.viewController else {
232x
179
                    return
!
180
                }
232x
181
                try slip.postTaskSlip.postTask.perform(on: viewController, with: slip.context, routingStack: viewControllers)
232x
182
            }
232x
183
        }
103x
184
    }
185
186
    final class GlobalTaskRunner {
187
188
        private final var interceptorRunner: InterceptorRunner
189
190
        private final let contextTaskRunner: ContextTaskRunner
191
192
        private final let postTaskRunner: PostTaskRunner
193
194
        init(interceptorRunner: InterceptorRunner, contextTaskRunner: ContextTaskRunner, postTaskRunner: PostTaskRunner) {
117x
195
            self.interceptorRunner = interceptorRunner
117x
196
            self.contextTaskRunner = contextTaskRunner
117x
197
            self.postTaskRunner = postTaskRunner
117x
198
        }
117x
199
200
        final func taskRunner(for step: PerformableStep?, with context: AnyContext) throws -> StepTaskTaskRunner {
270x
201
            guard let interceptableStep = step as? InterceptableStep else {
270x
202
                return StepTaskTaskRunner(contextTaskRunner: self.contextTaskRunner, postTaskRunner: self.postTaskRunner, context: context)
76x
203
            }
194x
204
            var contextTaskRunner = contextTaskRunner
194x
205
            var postTaskRunner = postTaskRunner
194x
206
            if let interceptor = interceptableStep.interceptor {
194x
207
                try interceptorRunner.add(interceptor, with: context)
27x
208
            }
194x
209
            if let contextTask = interceptableStep.contextTask {
194x
210
                try contextTaskRunner.add(contextTask, with: context)
106x
211
            }
194x
212
            if let postTask = interceptableStep.postTask {
194x
213
                try postTaskRunner.add(postTask)
13x
214
            }
194x
215
            return StepTaskTaskRunner(contextTaskRunner: contextTaskRunner, postTaskRunner: postTaskRunner, context: context)
194x
216
        }
194x
217
218
        final func performInterceptors(completion: @escaping (_: RoutingResult) -> Void) {
105x
219
            interceptorRunner.perform(completion: completion)
105x
220
        }
105x
221
222
        final func performPostTasks() throws {
101x
223
            try postTaskRunner.commit()
101x
224
        }
101x
225
226
    }
227
228
    /// Each post action needs to know a view controller is should be applied to.
229
    /// This decorator adds functionality of storing `UIViewController`s created by the `Factory` and frees
230
    /// custom factories implementations from dealing with it. Mostly it is important for ContainerFactories
231
    /// which create merged view controllers without `Router`'s help.
232
    struct FactoryDecorator: AnyFactory, CustomStringConvertible {
233
234
        private var factory: AnyFactory
235
236
        private let stepTaskRunner: StepTaskTaskRunner
237
238
        let action: AnyAction
239
240
        init(factory: AnyFactory, stepTaskRunner: StepTaskTaskRunner) {
134x
241
            self.factory = factory
134x
242
            self.action = factory.action
134x
243
            self.stepTaskRunner = stepTaskRunner
134x
244
        }
134x
245
246
        mutating func prepare(with context: AnyContext) throws {
134x
247
            try factory.prepare(with: context)
134x
248
        }
134x
249
250
        func build(with context: AnyContext) throws -> UIViewController {
128x
251
            let viewController = try factory.build(with: context)
128x
252
            try stepTaskRunner.perform(on: viewController)
128x
253
            return viewController
128x
254
        }
128x
255
256
        mutating func scrapeChildren(from factories: [(factory: AnyFactory, context: AnyContext)]) throws -> [(factory: AnyFactory, context: AnyContext)] {
134x
257
            try factory.scrapeChildren(from: factories)
134x
258
        }
134x
259
260
        var description: String {
96x
261
            String(describing: factory)
96x
262
        }
96x
263
264
    }
265
266
    final class DefaultPostponedIntegrationHandler: PostponedActionIntegrationHandler, MainThreadChecking {
267
268
        private(set) final var containerViewController: ContainerViewController?
269
270
        private(set) final var postponedViewControllers: [UIViewController] = []
105x
271
272
        final let logger: Logger?
273
274
        final let containerAdapterLocator: ContainerAdapterLocator
275
276
        init(logger: Logger?, containerAdapterLocator: ContainerAdapterLocator) {
105x
277
            self.logger = logger
105x
278
            self.containerAdapterLocator = containerAdapterLocator
105x
279
        }
105x
280
281
        final func update(containerViewController: ContainerViewController, animated: Bool, completion: @escaping (_: RoutingResult) -> Void) {
2x
282
            do {
2x
283
                guard self.containerViewController == nil else {
2x
284
                    purge(animated: animated, completion: { result in
!
285
                        self.assertIfNotMainThread()
!
286
                        guard result.isSuccessful else {
!
287
                            completion(result)
!
288
                            return
!
289
                        }
!
290
                        self.update(containerViewController: containerViewController, animated: animated, completion: completion)
!
291
                    })
!
292
                    return
!
293
                }
2x
294
                self.containerViewController = containerViewController
2x
295
                postponedViewControllers = try containerAdapterLocator.getAdapter(for: containerViewController).containedViewControllers
2x
296
                logger?.log(.info("Container \(String(describing: containerViewController)) will be used for the postponed integration."))
2x
297
                completion(.success)
2x
298
            } catch {
2x
299
                completion(.failure(error))
!
300
            }
2x
301
        }
2x
302
303
        final func update(postponedViewControllers: [UIViewController]) {
3x
304
            self.postponedViewControllers = postponedViewControllers
3x
305
        }
3x
306
307
        final func purge(animated: Bool, completion: @escaping (_: RoutingResult) -> Void) {
198x
308
            do {
198x
309
                guard let containerViewController else {
198x
310
                    completion(.success)
196x
311
                    return
196x
312
                }
196x
313
2x
314
                let containerAdapter = try containerAdapterLocator.getAdapter(for: containerViewController)
2x
315
2x
316
                guard !postponedViewControllers.isEqual(to: containerAdapter.containedViewControllers) else {
2x
317
                    reset()
!
318
                    completion(.success)
!
319
                    return
!
320
                }
2x
321
2x
322
                containerAdapter.setContainedViewControllers(postponedViewControllers,
2x
323
                                                             animated: animated,
2x
324
                                                             completion: { result in
2x
325
                                                                 guard result.isSuccessful else {
2x
326
                                                                     completion(result)
!
327
                                                                     return
!
328
                                                                 }
2x
329
                                                                 self.logger?.log(.info("View controllers \(String(describing: self.postponedViewControllers)) were simultaneously "
2x
330
                                                                         + "integrated into \(String(describing: containerViewController))"))
2x
331
                                                                 self.reset()
2x
332
                                                                 completion(.success)
2x
333
                                                             })
2x
334
            } catch {
2x
335
                completion(.failure(error))
!
336
            }
2x
337
        }
2x
338
339
        private final func reset() {
2x
340
            containerViewController = nil
2x
341
            postponedViewControllers = []
2x
342
        }
2x
343
344
    }
345
346
}