Slather logo

Coverage for "NavigationDelayInterceptor.swift" : 100.00%

(31 of 31 relevant lines covered)

RouteComposer/Classes/Extra/NavigationDelayInterceptor.swift

1
//
2
// RouteComposer
3
// NavigationDelayInterceptor.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
/// `NavigationDelayingInterceptor` delays the router from starting the navigation, while any view controllers in the
17
/// stack are being presented or dismissed. In case your app has some other navigation instruments rather than
18
/// `RouteComposer` or you have a situation when a few routers work simultaneously, add it to your **router** to avoid
19
/// the router not being able to navigate to the destination because a view controller in the stack is
20
/// being presented or dismissed.
21
///
22
/// *NB: `UIKit` does not allow simultaneous changes in `UIViewController` stack. The `.wait` strategy does not
23
/// guarantee 100% protection from all possible situations. The code must be written in a way that avoids such
24
/// situations. The `.wait` strategy can be used only as a temporary solution.*
25
public struct NavigationDelayingInterceptor<Context>: RoutingInterceptor {
26
27
    // MARK: Internal entities
28
29
    /// The strategy to be used by `NavigationDelayingInterceptor`
30
    ///
31
    /// - wait: Wait while some `UIViewController` is being presented or dismissed.
32
    /// - abort:  Abort tha navigation if some `UIViewController` is being presented or dismissed.
33
    public enum Strategy {
34
35
        /// Abort tha navigation if some `UIViewController` is being presented or dismissed.
36
        case abort
37
38
        /// Wait while some `UIViewController` is being presented or dismissed.
39
        case wait
40
41
    }
42
43
    // MARK: Properties
44
45
    /// `WindowProvider` instance.
46
    public let windowProvider: WindowProvider
47
48
    /// `Logger` instance.
49
    public let logger: Logger?
50
51
    /// Type of `Strategy`.
52
    public let strategy: Strategy
53
54
    // MARK: Methods
55
56
    /// Constructor
57
    ///
58
    /// - Parameters:
59
    ///   - windowProvider: `WindowProvider` instance.
60
    ///   - strategy: Type of `Strategy` to be used.
61
    ///   - logger: `Logger` instance.
62
    public init(windowProvider: WindowProvider = RouteComposerDefaults.shared.windowProvider,
63
                strategy: Strategy = .abort,
64
                logger: Logger? = RouteComposerDefaults.shared.logger) {
14x
65
        self.windowProvider = windowProvider
14x
66
        self.logger = logger
14x
67
        self.strategy = strategy
14x
68
    }
14x
69
70
    public func perform(with context: Context, completion: @escaping (RoutingResult) -> Void) {
105x
71
        guard let topmostViewController = getTopmostViewController(),
105x
72
              topmostViewController.isBeingDismissed || topmostViewController.isBeingPresented else {
105x
73
            completion(.success)
100x
74
            return
100x
75
        }
100x
76
        guard strategy == .wait else {
5x
77
            completion(.failure(RoutingError.compositionFailed(.init("\(topmostViewController) is changing its state. Navigation has been aborted."))))
1x
78
            return
1x
79
        }
4x
80
4x
81
        logger?.log(.info("\(topmostViewController) is changing its state. Navigation has been postponed."))
4x
82
        let deadline = DispatchTime.now() + .milliseconds(100)
4x
83
        DispatchQueue.main.asyncAfter(deadline: deadline) {
4x
84
            self.perform(with: context, completion: completion)
4x
85
        }
4x
86
    }
4x
87
88
    private func getTopmostViewController() -> UIViewController? {
105x
89
        var topmostViewController = windowProvider.window?.rootViewController
105x
90
105x
91
        while let presentedViewController = topmostViewController?.presentedViewController {
105x
92
            topmostViewController = presentedViewController
41x
93
        }
105x
94
105x
95
        return topmostViewController
105x
96
    }
105x
97
98
}