1 // Copyright (c) 2018, Applidium. All rights reserved 2 // OverlayViewController.swift 3 // OverlayContainer 4 // 5 // Created by Gaétan Zanella on 12/11/2018. 6 // 7 8 import UIKit 9 10 /// A `OverlayContainerViewController` is a container view controller that manages one or more 11 /// child view controllers in an overlay interface. 12 /// 13 /// It defines an area where a view controller, called the overlay view controller, 14 /// can be dragged up and down, hiding or revealing the content underneath it. 15 /// 16 /// OverlayContainer uses the last view controller of its viewControllers as the overlay view controller. 17 /// It stacks the other view controllers on top of each other, if any, and adds them underneath the overlay view controller. 18 open class OverlayContainerViewController: UIViewController { 19 20 /// `OverlayStyle` defines how the overlay view controller will be constrained in the container. 21 public enum OverlayStyle { 22 /// The overlay view controller will not be height-constrained. It will grow and shrink 23 /// as the user drags it up and down. 24 case flexibleHeight 25 /// The overlay view controller will be constrained with a height equal to the highest notch. 26 /// It will be fully visible only when the user has drag it up to this notch. 27 case rigid 28 /// The overlay view controller will be constrained with a height greater or equal to the highest notch. 29 /// Its height will be expanded if the overlay goes beyond the highest notch. 30 case expandableHeight 31 } 32 33 /// The delegate of the container. 34 weak open var delegate: OverlayContainerViewControllerDelegate? { 35 set { 36 configuration.delegate = newValue 37 configuration.invalidateOverlayMetrics() 38 setNeedsOverlayContainerHeightUpdate() 39 } 40 get { 41 return configuration.delegate 42 } 43 } 44 45 /// The view controllers displayed. 46 open var viewControllers: [UIViewController] = [] { 47 didSet { 48 guard isViewLoaded else { return } 49 oldValue.forEach { removeChild($0) } 50 loadOverlayViews() 51 setNeedsStatusBarAppearanceUpdate() 52 } 53 } 54 55 /// The overlay view controller 56 open var topViewController: UIViewController? { 57 return viewControllers.last 58 } 59 60 open override var childForStatusBarStyle: UIViewController? { 61 return topViewController 62 } 63 64 /// The scroll view managing the overlay translation. 65 weak open var drivingScrollView: UIScrollView? { 66 didSet { 67 guard drivingScrollView !== oldValue else { return } 68 guard isViewLoaded else { return } 69 loadTranslationDrivers() 70 } 71 } 72 73 /// The height of the area where the overlay view controller can be dragged up and down. 74 /// It will only be valid once the container view is laid out or in the delegate callbacks. 75 open var availableSpace: CGFloat { 76 return view.frame.height 77 } 78 79 /// The style of the container. 80 public let style: OverlayStyle 81 82 private lazy var overlayPanGesture: OverlayTranslationGestureRecognizer = self.makePanGesture() 83 private lazy var overlayContainerView = OverlayContainerView() 84 private lazy var overlayTranslationView = OverlayTranslationView() 85 private lazy var overlayTranslationContainerView = OverlayTranslationContainerView() 86 private lazy var groundView = GroundView() 87 88 private var overlayContainerViewStyleConstraint: NSLayoutConstraint? 89 private var translationHeightConstraint: NSLayoutConstraint? 90 91 private lazy var configuration = makeConfiguration() 92 93 private var needsOverlayContainerHeightUpdate = true 94 95 private var previousSize: CGSize = .zero 96 private var translationController: HeightConstraintOverlayTranslationController? 97 private var translationDrivers: [OverlayTranslationDriver] = [] 98 99 // (gz) 2020-08-11 Uses to determine whether we can safely call `presentationController` or not. 100 // See issue #72 101 private var isPresentedInsideAnOverlayContainerPresentationController = false 102 103 // MARK: - Life Cycle 104 105 /// Creates an instance with the specified `style`. 106 /// 107 /// - parameter style: The style used by the container. The default value is `expandableHeight`. 108 /// 109 /// - returns: The new `OverlayContainerViewController` instance. 110 public init(style: OverlayStyle = .expandableHeight) { 111 self.style = style 112 super.init(nibName: nil, bundle: nil) 113 } 114 115 public required init?(coder aDecoder: NSCoder) { 116 self.style = .flexibleHeight 117 super.init(coder: aDecoder) 118 } 119 120 // MARK: - UIViewController 121 loadViewnull122 open override func loadView() { 123 view = PassThroughView() 124 view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 125 loadContainerViews() 126 loadOverlayViews() 127 } 128 viewDidLoadnull129 open override func viewDidLoad() { 130 super.viewDidLoad() 131 setUpPanGesture() 132 } 133 viewWillLayoutSubviewsnull134 open override func viewWillLayoutSubviews() { 135 // (gz) 2019-06-10 According to the documentation, the default implementation of 136 // `viewWillLayoutSubviews` does nothing. 137 // Nethertheless in its `Changing Constraints` Guide, Apple recommends to call it. 138 defer { 139 super.viewWillLayoutSubviews() 140 } 141 let hasNewHeight = previousSize.height != view.bounds.size.height 142 let hasPendingTranslation = translationController?.hasPendingTranslation() == true 143 guard needsOverlayContainerHeightUpdate || hasNewHeight else { return } 144 needsOverlayContainerHeightUpdate = false 145 previousSize = view.bounds.size 146 if hasNewHeight { 147 configuration.invalidateOverlayMetrics() 148 } 149 if hasNewHeight && !hasPendingTranslation { 150 translationController?.scheduleOverlayTranslation( 151 .toLastReachedNotchIndex, 152 velocity: .zero, 153 animated: false 154 ) 155 } 156 configuration.requestOverlayMetricsIfNeeded() 157 performDeferredTranslations() 158 } 159 160 // MARK: - Internal 161 overlayContainerPresentationTransitionWillBeginnull162 func overlayContainerPresentationTransitionWillBegin() { 163 isPresentedInsideAnOverlayContainerPresentationController = true 164 } 165 overlayContainerDismissalTransitionDidEndnull166 func overlayContainerDismissalTransitionDidEnd() { 167 isPresentedInsideAnOverlayContainerPresentationController = false 168 } 169 170 // MARK: - Public 171 172 /// Moves the overlay view controller to the specified notch. 173 /// 174 /// - parameter index: The index of the target notch. 175 /// - parameter animated: Defines either the transition should be animated or not. 176 /// - parameter completion: The block to execute after the translation finishes. 177 /// This block has no return value and takes no parameters. You may specify nil for this parameter. 178 /// 179 open func moveOverlay(toNotchAt index: Int, animated: Bool, completion: (() -> Void)? = nil) { 180 loadViewIfNeeded() 181 translationController?.scheduleOverlayTranslation( 182 .toIndex(index), 183 velocity: .zero, 184 animated: animated, 185 completion: completion 186 ) 187 setNeedsOverlayContainerHeightUpdate() 188 } 189 190 /// Invalidates the current container notches. 191 /// 192 /// This method does not reload the notch heights immediately. The changes are scheduled to the next layout pass. 193 /// By default, the overlay container will use its target notch policy to determine where to go 194 /// and animates the translation. 195 /// Use `moveOverlay(toNotchAt:animated:completion:)` to override this behavior. 196 /// invalidateNotchHeightsnull197 open func invalidateNotchHeights() { 198 guard isViewLoaded else { return } 199 configuration.invalidateOverlayMetrics() 200 translationController?.scheduleOverlayTranslation( 201 .basedOnTargetPolicy, 202 velocity: .zero, 203 animated: true 204 ) 205 setNeedsOverlayContainerHeightUpdate() 206 } 207 208 // MARK: - Private 209 loadContainerViewsnull210 private func loadContainerViews() { 211 view.addSubview(groundView) 212 groundView.pinToSuperview() 213 view.addSubview(overlayTranslationContainerView) 214 overlayTranslationContainerView.pinToSuperview() 215 overlayTranslationContainerView.addSubview(overlayTranslationView) 216 overlayTranslationView.addSubview(overlayContainerView) 217 overlayTranslationView.pinToSuperview(edges: [.bottom, .left, .right]) 218 overlayContainerView.pinToSuperview(edges: [.left, .top, .right]) 219 translationHeightConstraint = overlayTranslationView.heightAnchor.constraint(equalToConstant: 0) 220 switch style { 221 case .flexibleHeight: 222 overlayContainerViewStyleConstraint = overlayContainerView.bottomAnchor.constraint( 223 equalTo: overlayTranslationView.bottomAnchor 224 ) 225 case .rigid: 226 overlayContainerViewStyleConstraint = overlayContainerView.heightAnchor.constraint( 227 equalToConstant: 0 228 ) 229 case .expandableHeight: 230 overlayContainerViewStyleConstraint = overlayContainerView.heightAnchor.constraint( 231 equalToConstant: 0 232 ) 233 overlayContainerViewStyleConstraint?.priority = .defaultHigh 234 let bottomConstraint = overlayContainerView.bottomAnchor.constraint( 235 greaterThanOrEqualTo: overlayTranslationView.bottomAnchor 236 ) 237 bottomConstraint.isActive = true 238 } 239 loadTranslationController() 240 } 241 loadTranslationControllernull242 private func loadTranslationController() { 243 guard let translationHeightConstraint = translationHeightConstraint else { return } 244 translationController = HeightConstraintOverlayTranslationController( 245 translationHeightConstraint: translationHeightConstraint, 246 configuration: configuration 247 ) 248 translationController?.delegate = self 249 translationController?.scheduleOverlayTranslation( 250 .toIndex(0), 251 velocity: .zero, 252 animated: false 253 ) 254 } 255 loadOverlayViewsnull256 private func loadOverlayViews() { 257 guard !viewControllers.isEmpty else { return } 258 groundView.isHidden = viewControllers.count == 1 259 var truncatedViewControllers = viewControllers 260 truncatedViewControllers.popLast().flatMap { addChild($0, in: overlayContainerView) } 261 truncatedViewControllers.forEach { addChild($0, in: groundView) } 262 loadTranslationDrivers() 263 } 264 loadTranslationDriversnull265 private func loadTranslationDrivers() { 266 guard let translationController = translationController, 267 let overlayController = topViewController else { 268 return 269 } 270 translationDrivers.forEach { $0.clean() } 271 translationDrivers.removeAll() 272 var drivers: [OverlayTranslationDriver] = [] 273 let panGestureDriver = PanGestureOverlayTranslationDriver( 274 translationController: translationController, 275 panGestureRecognizer: overlayPanGesture 276 ) 277 drivers.append(panGestureDriver) 278 let scrollView = drivingScrollView ?? configuration.scrollView(drivingOverlay: overlayController) 279 if let scrollView = scrollView { 280 overlayPanGesture.drivingScrollView = scrollView 281 let driver = ScrollViewOverlayTranslationDriver( 282 translationController: translationController, 283 scrollView: scrollView 284 ) 285 drivers.append(driver) 286 } 287 translationDrivers = drivers 288 } 289 setNeedsOverlayContainerHeightUpdatenull290 private func setNeedsOverlayContainerHeightUpdate() { 291 needsOverlayContainerHeightUpdate = true 292 viewIfLoaded?.setNeedsLayout() 293 } 294 updateOverlayContainerConstraintsnull295 private func updateOverlayContainerConstraints() { 296 switch style { 297 case .flexibleHeight: 298 overlayContainerViewStyleConstraint?.constant = 0 299 case .rigid, .expandableHeight: 300 overlayContainerViewStyleConstraint?.constant = configuration.maximumNotchHeight 301 } 302 translationHeightConstraint?.isActive = true 303 overlayContainerViewStyleConstraint?.isActive = true 304 } 305 performDeferredTranslationsnull306 private func performDeferredTranslations() { 307 translationController?.performDeferredTranslations() 308 } 309 setUpPanGesturenull310 private func setUpPanGesture() { 311 view.addGestureRecognizer(overlayPanGesture) 312 } 313 makeConfigurationnull314 private func makeConfiguration() -> OverlayContainerConfigurationImplementation { 315 return OverlayContainerConfigurationImplementation( 316 overlayContainerViewController: self 317 ) 318 } 319 makePanGesturenull320 private func makePanGesture() -> OverlayTranslationGestureRecognizer { 321 return OverlayTranslationGestureRecognizer() 322 } 323 } 324 325 extension OverlayContainerViewController: HeightConstraintOverlayTranslationControllerDelegate { 326 327 private var overlayPresentationController: OverlayContainerPresentationController? { 328 guard isPresentedInsideAnOverlayContainerPresentationController else { return nil } 329 return oc_findPresentationController(OverlayContainerPresentationController.self) 330 } 331 332 // MARK: - HeightOverlayTranslationControllerDelegate 333 334 func translationController(_ translationController: OverlayTranslationController, 335 didMoveOverlayToNotchAt index: Int) { 336 guard let controller = topViewController else { return } 337 delegate?.overlayContainerViewController(self, didMoveOverlay: controller, toNotchAt: index) 338 overlayPresentationController?.overlayContainerViewController( 339 self, 340 didMoveOverlay: controller, 341 toNotchAt: index 342 ) 343 } 344 345 func translationController(_ translationController: OverlayTranslationController, 346 willMoveOverlayToNotchAt index: Int) { 347 guard let controller = topViewController else { return } 348 delegate?.overlayContainerViewController(self, willMoveOverlay: controller, toNotchAt: index) 349 overlayPresentationController?.overlayContainerViewController( 350 self, 351 willMoveOverlay: controller, 352 toNotchAt: index 353 ) 354 } 355 translationControllerWillStartDraggingOverlaynull356 func translationControllerWillStartDraggingOverlay(_ translationController: OverlayTranslationController) { 357 guard let controller = topViewController else { return } 358 delegate?.overlayContainerViewController( 359 self, 360 willStartDraggingOverlay: controller 361 ) 362 overlayPresentationController?.overlayContainerViewController( 363 self, 364 willStartDraggingOverlay: controller 365 ) 366 } 367 368 func translationController(_ translationController: OverlayTranslationController, 369 willEndDraggingAtVelocity velocity: CGPoint) { 370 guard let controller = topViewController else { return } 371 delegate?.overlayContainerViewController( 372 self, 373 willEndDraggingOverlay: controller, 374 atVelocity: velocity 375 ) 376 overlayPresentationController?.overlayContainerViewController( 377 self, 378 willEndDraggingOverlay: controller, 379 atVelocity: velocity 380 ) 381 } 382 383 func translationController(_ translationController: OverlayTranslationController, 384 willTranslateOverlayWith transitionCoordinator: OverlayContainerTransitionCoordinator) { 385 guard let controller = topViewController else { return } 386 if transitionCoordinator.isAnimated { 387 overlayTranslationContainerView.layoutIfNeeded() 388 } 389 transitionCoordinator.animate(alongsideTransition: { [weak self] context in 390 self?.updateOverlayContainerConstraints() 391 self?.overlayTranslationContainerView.layoutIfNeeded() 392 }, completion: nil) 393 delegate?.overlayContainerViewController( 394 self, 395 willTranslateOverlay: controller, 396 transitionCoordinator: transitionCoordinator 397 ) 398 overlayPresentationController?.overlayContainerViewController( 399 self, 400 willTranslateOverlay: controller, 401 transitionCoordinator: transitionCoordinator 402 ) 403 } 404 translationControllerDidScheduleTranslationsnull405 func translationControllerDidScheduleTranslations(_ translationController: OverlayTranslationController) { 406 setNeedsOverlayContainerHeightUpdate() 407 } 408 overlayViewControllernull409 func overlayViewController(for translationController: OverlayTranslationController) -> UIViewController? { 410 return topViewController 411 } 412 } 413