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