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 |
}
|
|