Slather logo

Coverage for "UIViewController+Action.swift" : 100.00%

(108 of 108 relevant lines covered)

RouteComposer/Classes/Actions/UIViewController+Action.swift

1
//
2
// RouteComposer
3
// UIViewController+Action.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
/// A wrapper for general actions that can be applied to any `UIViewController`
17
public enum GeneralAction {
18
19
    // MARK: Actions
20
21
    /// Replaces the root view controller in the key `UIWindow`
22
    ///
23
    /// - Parameters:
24
    ///   - windowProvider: `WindowProvider` instance
25
    ///   - animationOptions: Set of `UIView.AnimationOptions`. Transition will happen without animation if not provided.
26
    ///   - duration: Transition duration.
27
    public static func replaceRoot(windowProvider: WindowProvider = RouteComposerDefaults.shared.windowProvider,
28
                                   animationOptions: UIView.AnimationOptions? = nil,
29
                                   duration: TimeInterval = 0.3) -> ViewControllerActions.ReplaceRootAction {
68x
30
        ViewControllerActions.ReplaceRootAction(windowProvider: windowProvider, animationOptions: animationOptions, duration: duration)
68x
31
    }
68x
32
33
    /// Presents a view controller modally
34
    ///
35
    /// - Parameters:
36
    ///   - presentationStartingPoint: A starting point in the modal presentation
37
    ///   - presentationStyle: `UIModalPresentationStyle` setting, default value: .fullScreen
38
    ///   - transitionStyle: `UIModalTransitionStyle` setting, default value: .coverVertical
39
    ///   - transitioningDelegate: `UIViewControllerTransitioningDelegate` instance to be used during the transition
40
    ///   - isModalInPresentation: A Boolean value indicating whether the view controller enforces a modal behavior.
41
    ///   - preferredContentSize: The preferredContentSize is used for any container laying out a child view controller.
42
    ///   - popoverControllerConfigurationBlock: Block to configure `UIPopoverPresentationController`.
43
    public static func presentModally(startingFrom presentationStartingPoint: ViewControllerActions.PresentModallyAction.ModalPresentationStartingPoint = .current,
44
                                      presentationStyle: UIModalPresentationStyle? = .fullScreen,
45
                                      transitionStyle: UIModalTransitionStyle? = .coverVertical,
46
                                      transitioningDelegate: UIViewControllerTransitioningDelegate? = nil,
47
                                      preferredContentSize: CGSize? = nil,
48
                                      isModalInPresentation: Bool? = nil,
49
                                      popoverConfiguration: ((_: UIPopoverPresentationController) -> Void)? = nil) -> ViewControllerActions.PresentModallyAction {
43x
50
        ViewControllerActions.PresentModallyAction(startingFrom: presentationStartingPoint,
43x
51
                                                   presentationStyle: presentationStyle,
43x
52
                                                   transitionStyle: transitionStyle,
43x
53
                                                   transitioningDelegate: transitioningDelegate,
43x
54
                                                   preferredContentSize: preferredContentSize,
43x
55
                                                   isModalInPresentation: isModalInPresentation,
43x
56
                                                   popoverConfiguration: popoverConfiguration)
43x
57
    }
43x
58
59
    /// `Action` does nothing, but can be helpful for testing or writing the sequences of steps with the `NilFactory`
60
    public static func nilAction() -> ViewControllerActions.NilAction {
1x
61
        ViewControllerActions.NilAction()
1x
62
    }
1x
63
64
}
65
66
/// A wrapper for general actions that can be applied to any `UIViewController`
67
public enum ViewControllerActions {
68
69
    // MARK: Internal entities
70
71
    /// Presents a view controller modally
72
    public struct PresentModallyAction: Action {
73
74
        /// A starting point in the modal presentation
75
        public enum ModalPresentationStartingPoint {
76
77
            /// Present from the `UIViewController` from the previous step (Default behaviour)
78
            case current
79
80
            /// Present from the topmost parent `UIViewController` of the `UIViewController` from the previous step
81
            case topmostParent
82
83
            /// Present from the custom `UIViewController`
84
            case custom(@autoclosure () throws -> UIViewController?)
85
86
        }
87
88
        // MARK: Properties
89
90
        /// A starting point in the modal presentation
91
        public let presentationStartingPoint: ModalPresentationStartingPoint
92
93
        /// `UIModalPresentationStyle` setting
94
        public let presentationStyle: UIModalPresentationStyle?
95
96
        /// A Boolean value indicating whether the view controller enforces a modal behavior.
97
        public let isModalInPresentation: Bool?
98
99
        /// `UIModalTransitionStyle` setting
100
        public let transitionStyle: UIModalTransitionStyle?
101
102
        /// The preferredContentSize is used for any container laying out a child view controller.
103
        public let preferredContentSize: CGSize?
104
105
        /// Block to configure `UIPopoverPresentationController`
106
        public let popoverControllerConfigurationBlock: ((_: UIPopoverPresentationController) -> Void)?
107
108
        /// `UIViewControllerTransitioningDelegate` instance to be used during the transition
109
        public private(set) weak var transitioningDelegate: UIViewControllerTransitioningDelegate?
110
111
        // MARK: Methods
112
113
        /// Constructor
114
        ///
115
        /// - Parameters:
116
        ///   - presentationStartingPoint: A starting point in the modal presentation
117
        ///   - presentationStyle: `UIModalPresentationStyle` setting, default value: .fullScreen
118
        ///   - transitionStyle: `UIModalTransitionStyle` setting, default value: .coverVertical
119
        ///   - transitioningDelegate: `UIViewControllerTransitioningDelegate` instance to be used during the transition
120
        ///   - preferredContentSize: The preferredContentSize is used for any container laying out a child view controller.
121
        ///   - isModalInPresentation: A Boolean value indicating whether the view controller enforces a modal behavior.
122
        ///   - popoverControllerConfigurationBlock: Block to configure `UIPopoverPresentationController`.
123
        init(startingFrom presentationStartingPoint: ModalPresentationStartingPoint = .current,
124
             presentationStyle: UIModalPresentationStyle? = .fullScreen,
125
             transitionStyle: UIModalTransitionStyle? = .coverVertical,
126
             transitioningDelegate: UIViewControllerTransitioningDelegate? = nil,
127
             preferredContentSize: CGSize? = nil,
128
             isModalInPresentation: Bool? = nil,
129
             popoverConfiguration: ((_: UIPopoverPresentationController) -> Void)? = nil) {
44x
130
            self.presentationStartingPoint = presentationStartingPoint
44x
131
            self.presentationStyle = presentationStyle
44x
132
            self.transitionStyle = transitionStyle
44x
133
            self.transitioningDelegate = transitioningDelegate
44x
134
            self.preferredContentSize = preferredContentSize
44x
135
            self.popoverControllerConfigurationBlock = popoverConfiguration
44x
136
            self.isModalInPresentation = isModalInPresentation
44x
137
        }
44x
138
139
        public func perform(with viewController: UIViewController,
140
                            on existingController: UIViewController,
141
                            animated: Bool,
142
                            completion: @escaping (_: RoutingResult) -> Void) {
25x
143
25x
144
            let presentingViewController: UIViewController
25x
145
            switch presentationStartingPoint {
25x
146
            case .current:
25x
147
                presentingViewController = existingController
14x
148
            case .topmostParent:
25x
149
                presentingViewController = existingController.allParents.last ?? existingController
3x
150
            case let .custom(viewController):
25x
151
                guard let viewController = try? viewController() else {
8x
152
                    completion(.failure(RoutingError.compositionFailed(
1x
153
                        .init("The view controller to start modal presentation from was not found."))))
1x
154
                    return
1x
155
                }
7x
156
                presentingViewController = viewController
7x
157
            }
25x
158
25x
159
            guard presentingViewController.presentedViewController == nil else {
25x
160
                completion(.failure(RoutingError.compositionFailed(.init("\(presentingViewController) is " +
1x
161
                        "already presenting a view controller."))))
1x
162
                return
1x
163
            }
24x
164
            if let presentationStyle {
24x
165
                viewController.modalPresentationStyle = presentationStyle
23x
166
            }
24x
167
            if let transitionStyle {
24x
168
                viewController.modalTransitionStyle = transitionStyle
23x
169
            }
24x
170
            if let transitioningDelegate {
24x
171
                viewController.transitioningDelegate = transitioningDelegate
4x
172
            }
24x
173
            if let preferredContentSize {
24x
174
                viewController.preferredContentSize = preferredContentSize
1x
175
            }
24x
176
            if let popoverPresentationController = viewController.popoverPresentationController,
24x
177
               let popoverControllerConfigurationBlock {
24x
178
                popoverControllerConfigurationBlock(popoverPresentationController)
1x
179
            }
24x
180
            if #available(iOS 13, *),
24x
181
               let isModalInPresentation {
24x
182
                viewController.isModalInPresentation = isModalInPresentation
1x
183
            }
24x
184
24x
185
            presentingViewController.present(viewController, animated: animated, completion: {
24x
186
                completion(.success)
23x
187
            })
23x
188
        }
24x
189
190
    }
191
192
    /// Replaces the root view controller in the key `UIWindow`
193
    public struct ReplaceRootAction: Action {
194
195
        // MARK: Properties
196
197
        /// `WindowProvider` instance
198
        public let windowProvider: WindowProvider
199
200
        /// Set of `UIView.AnimationOptions`
201
        public let animationOptions: UIView.AnimationOptions?
202
203
        /// Transition duration.
204
        public let duration: TimeInterval
205
206
        // MARK: Methods
207
208
        /// Constructor
209
        ///
210
        /// - Parameters:
211
        ///   - windowProvider: `WindowProvider` instance
212
        ///   - animationOptions: Set of `UIView.AnimationOptions`. Transition will happen without animation if not provided.
213
        ///   - duration: Transition duration.
214
        init(windowProvider: WindowProvider = RouteComposerDefaults.shared.windowProvider, animationOptions: UIView.AnimationOptions? = nil, duration: TimeInterval = 0.3) {
71x
215
            self.windowProvider = windowProvider
71x
216
            self.animationOptions = animationOptions
71x
217
            self.duration = duration
71x
218
        }
71x
219
220
        public func perform(with viewController: UIViewController,
221
                            on existingController: UIViewController,
222
                            animated: Bool,
223
                            completion: @escaping (_: RoutingResult) -> Void) {
30x
224
            guard let window = windowProvider.window else {
30x
225
                completion(.failure(RoutingError.compositionFailed(.init("Window was not found."))))
1x
226
                return
1x
227
            }
29x
228
            guard window.rootViewController == existingController else {
29x
229
                completion(.failure(RoutingError.compositionFailed(.init("Action should be applied to the root view " +
3x
230
                        "controller, got \(String(describing: existingController)) instead."))))
3x
231
                return
3x
232
            }
26x
233
26x
234
            guard animated, let animationOptions, duration > 0 else {
26x
235
                window.rootViewController = viewController
10x
236
                window.makeKeyAndVisible()
10x
237
                completion(.success)
10x
238
                return
10x
239
            }
16x
240
16x
241
            UIView.transition(with: window, duration: duration, options: animationOptions, animations: {
16x
242
                let oldAnimationState = UIView.areAnimationsEnabled
16x
243
                UIView.setAnimationsEnabled(false)
16x
244
                window.rootViewController = viewController
16x
245
                window.rootViewController?.view.setNeedsLayout()
16x
246
                window.makeKeyAndVisible()
16x
247
                UIView.setAnimationsEnabled(oldAnimationState)
16x
248
            })
16x
249
            completion(.success)
16x
250
        }
16x
251
252
    }
253
254
    /// Helper `Action` that does nothing
255
    public struct NilAction: Action {
256
257
        // MARK: Methods
258
259
        /// Constructor
260
        init() {}
177x
261
262
        /// Does nothing and always succeeds
263
        public func perform(with viewController: UIViewController, on existingController: UIViewController, animated: Bool, completion: @escaping (RoutingResult) -> Void) {
21x
264
            completion(.success)
21x
265
        }
21x
266
267
    }
268
269
}