Slather logo

Coverage for "SwitchAssembly.swift" : 100.00%

(70 of 70 relevant lines covered)

RouteComposer/Classes/Assemblies/SwitchAssembly.swift

1
//
2
// RouteComposer
3
// SwitchAssembly.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
/// Builds a `DestinationStep` which can contain the conditions to select the steps to be taken by a `Router`.
17
/// ### Usage
18
/// ```swift
19
///        let containerStep = SwitchAssembly<UINavigationController, ProductContext>()
20
///                .addCase { (context: ProductContext) in
21
///                    // If this configuration is requested by a Universal Link (productURL != nil), skip this case otherwise.
22
///                    guard context.productURL != nil else {
23
///                        return nil
24
///                    }
25
///
26
///                    return ChainAssembly.from(NavigationControllerStep<UINavigationController, ProductContext>())
27
///                            .using(GeneralAction.presentModally())
28
///                            .from(GeneralStep.current())
29
///                            .assemble()
30
///
31
///                }
32
///
33
///                // If UINavigationController is visible on the screen - use it
34
///                .addCase(from: ClassFinder<UINavigationController, ProductContext>(options: .currentVisibleOnly))
35
///
36
///                // Otherwise - create a UINavigationController and present modally
37
///                .assemble(default: ChainAssembly.from(NavigationControllerStep<UINavigationController, ProductContext>())
38
///                    .using(GeneralAction.presentModally())
39
///                    .from(GeneralStep.current())
40
///                    .assemble())
41
/// ```
42
public final class SwitchAssembly<ViewController: UIViewController, Context> {
43
44
    // MARK: Internal entities
45
46
    private struct BlockResolver: StepCaseResolver {
47
48
        let resolverBlock: (_: Context) -> DestinationStep<ViewController, Context>?
49
50
        init(resolverBlock: @escaping (_: Context) -> DestinationStep<ViewController, Context>?) {
19x
51
            self.resolverBlock = resolverBlock
19x
52
        }
19x
53
54
        func resolve(with context: AnyContext) -> RoutingStep? {
18x
55
            guard let typedContext = try? context.value() as Context else {
18x
56
                return nil
1x
57
            }
17x
58
            return resolverBlock(typedContext)
17x
59
        }
18x
60
    }
61
62
    private struct FinderResolver<ViewController: UIViewController, Context>: StepCaseResolver {
63
64
        private let finder: AnyFinder?
65
66
        private let step: DestinationStep<ViewController, Context>
67
68
        init(finder: some Finder, step: DestinationStep<ViewController, Context>?) {
11x
69
            self.step = step ?? DestinationStep(GeneralStep.FinderStep(finder: finder))
11x
70
            self.finder = FinderBox(finder)
11x
71
        }
11x
72
73
        func resolve(with context: AnyContext) -> RoutingStep? {
12x
74
            guard (try? finder?.findViewController(with: context)) != nil else {
12x
75
                return nil
3x
76
            }
9x
77
            return step
9x
78
        }
12x
79
    }
80
81
    // MARK: Properties
82
83
    private var resolvers: [StepCaseResolver] = []
12x
84
85
    // MARK: Methods
86
87
    /// Constructor
88
    public init() {}
12x
89
90
    /// Adds a block that allows a written decision case for the `Router` in the block.
91
    /// Returning nil from the block will mean that it has not succeeded.
92
    ///
93
    /// - Parameter resolverBlock: case resolver block
94
    public final func addCase(_ resolverBlock: @escaping (_: Context) -> DestinationStep<ViewController, Context>?) -> Self {
1x
95
        resolvers.append(BlockResolver(resolverBlock: resolverBlock))
1x
96
        return self
1x
97
    }
1x
98
99
    /// Adds a case when a view controller exists in the stack in order to make a particular `DestinationStep`.
100
    ///
101
    /// - Parameters:
102
    ///   - finder: The `Finder` instance searches for a `UIViewController` in the stack
103
    ///   - step: The `DestinationStep` is to perform if the `Finder` has been able to find a view controller in the stack. If not provided,
104
    ///   a `UIViewController` found by the `Finder` will be considered as a view controller to start the navigation process from
105
    public final func addCase<F: Finder>(when finder: F, from step: DestinationStep<ViewController, Context>) -> Self where F.Context == Context {
2x
106
        resolvers.append(FinderResolver(finder: finder, step: step))
2x
107
        return self
2x
108
    }
2x
109
110
    /// Adds a case when a certain condition is valid to use a particular `DestinationStep`.
111
    ///
112
    /// - Parameters:
113
    ///   - condition: A condition to use the provided step.
114
    ///   - step: The `DestinationStep` is to perform.
115
    public final func addCase(when condition: @autoclosure @escaping () -> Bool, from step: DestinationStep<ViewController, Context>) -> Self {
1x
116
        resolvers.append(BlockResolver(resolverBlock: { _ in
2x
117
            guard condition() else {
2x
118
                return nil
1x
119
            }
1x
120
            return step
1x
121
        }))
2x
122
        return self
1x
123
    }
1x
124
125
    /// Adds a case when a certain condition is valid to use a particular `DestinationStep`.
126
    ///
127
    /// - Parameters:
128
    ///   - conditionBlock: A condition to use the provided step.
129
    ///   - step: The `DestinationStep` is to perform.
130
    public final func addCase(when conditionBlock: @escaping (Context) -> Bool, from step: DestinationStep<ViewController, Context>) -> Self {
8x
131
        resolvers.append(BlockResolver(resolverBlock: { context in
12x
132
            guard conditionBlock(context) else {
12x
133
                return nil
11x
134
            }
11x
135
            return step
1x
136
        }))
12x
137
        return self
8x
138
    }
8x
139
140
    /// Adds a case when a view controller exists - navigation will start from the resulting view controller.
141
    ///
142
    /// - Parameters:
143
    ///   - finder: The `Finder` instance is to find a `UIViewController` in the stack
144
    ///   a `UIViewController` found by the `Finder` will be considered as a view controller to start the navigation process from
145
    public final func addCase<F: Finder>(from finder: F) -> Self where F.ViewController == ViewController, F.Context == Context {
8x
146
        resolvers.append(FinderResolver<ViewController, Context>(finder: finder, step: nil))
8x
147
        return self
8x
148
    }
8x
149
150
    /// Assembles all the cases into a `DestinationStep` implementation
151
    ///
152
    /// - Returns: instance of a `DestinationStep`
153
    public final func assemble() -> DestinationStep<ViewController, Context> {
3x
154
        DestinationStep(SwitcherStep(resolvers: resolvers))
3x
155
    }
3x
156
157
    /// Assembles all the cases in a `DestinationStep` instance and adds the default implementation, providing the step it is to perform
158
    ///
159
    /// - Parameter resolverBlock: default resolver block
160
    /// - Returns: an instance of `DestinationStep`
161
    public final func assemble(default resolverBlock: @escaping () -> DestinationStep<ViewController, Context>) -> DestinationStep<ViewController, Context> {
2x
162
        resolvers.append(BlockResolver(resolverBlock: { _ in
2x
163
            resolverBlock()
1x
164
        }))
1x
165
        return DestinationStep(SwitcherStep(resolvers: resolvers))
2x
166
    }
2x
167
168
    /// Assembles all the cases in a `DestinationStep` instance and adds the default implementation, providing the step it is to perform
169
    ///
170
    /// - Parameter step: an instance of `DestinationStep`
171
    /// - Returns: a final instance of `DestinationStep`
172
    public final func assemble(default step: DestinationStep<ViewController, Context>) -> DestinationStep<ViewController, Context> {
7x
173
        resolvers.append(BlockResolver(resolverBlock: { _ in
7x
174
            step
2x
175
        }))
2x
176
        return DestinationStep(SwitcherStep(resolvers: resolvers))
7x
177
    }
7x
178
179
}
180
181
// MARK: Methods for ContainerViewController
182
183
public extension SwitchAssembly where ViewController: ContainerViewController {
184
185
    /// Adds a case when a view controller exists - navigation will start from the resulting view controller.
186
    /// This method allows to avoid view controller type check.
187
    ///
188
    /// - Parameters:
189
    ///   - finder: The `Finder` instance is to find a `UIViewController` in the stack
190
    ///   a `UIViewController` found by the `Finder` will be considered as a view controller to start the navigation process from
191
    final func addCase<F: Finder>(expecting finder: F) -> Self where F.Context == Context {
1x
192
        resolvers.append(FinderResolver<ViewController, Context>(finder: finder, step: nil))
1x
193
        return self
1x
194
    }
1x
195
196
}