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