1 //  Copyright (c) 2018, Applidium. All rights reserved
2 //  OverlayContainerSheetPresentationController.swift
3 //  OverlayContainer
4 //
5 //  Created by Gaétan Zanella on 08/04/2020.
6 //
7 
8 import UIKit
9 
10 /// An `OverlayContainerPresentationController` subclass that can be used to manage the transition animations and the presentation of overlay containers onscreen.
11 ///
12 /// It adds a dimming layer over the presenting content and changes its appearance based on the current container translations.
13 /// It also includes two dismissal gestures: tap-to-dismiss and drag-to-dismiss.
14 ///
15 /// You can subclass this class if you need an extra level of customization.
16 open class OverlayContainerSheetPresentationController: OverlayContainerPresentationController {
17 
18     // MARK: - Public properties
19 
20     /// The delegate of the presentation controller.
21     open weak var sheetDelegate: OverlayContainerSheetPresentationControllerDelegate?
22 
23     /// A dimming view added in the container view.
24     ///
25     /// The default implementation of this property returns the view you provided in the `init` method.
26     open private(set) var dimmingView: OverlayContainerSheetDimmingView?
27 
28     /// The underlying gesture recognizer for tap-to-dismiss gesture.
29     open private(set) lazy var dismissingTapGestureRecognizer: UITapGestureRecognizer = self.makeTapGestureRecognizer()
30 
31     // MARK: - Private properties
32 
33     private lazy var tapGestureRecognizerView: UIView = self.makeTapGestureRecognizerView()
34 
35     // MARK: - Life Cycle
36 
37     /// Initializes and returns a presentation controller for transitioning between the specified view controllers.
38     ///
39     /// - parameter dimmingView: The view used as a dimming view. The default value is a `TransparentOverlayContainerPresentationDimmingView` instance.
40     /// - parameter presentedViewController: The view controller being presented modally.
41     /// - parameter dimmingView: The view controller whose content represents the starting point of the transition.
42     ///
43     /// - returns: The new `OverlayContainerPresentationController` instance.
44     ///
45     /// The `presentedViewController` can be an overlay container or the parent view controller of an overlay container.
46     public init(dimmingView: OverlayContainerSheetDimmingView? = TransparentOverlayContainerSheetDimmingView(),
47                 presentedViewController: UIViewController,
48                 presenting: UIViewController?) {
49         self.dimmingView = dimmingView
50         super.init(
51             presentedViewController: presentedViewController,
52             presenting: presenting
53         )
54     }
55 
56     // MARK: - OverlayContainerPresentationController
57 
58     open override func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
59                                                       willTranslateOverlay overlayViewController: UIViewController,
60                                                       transitionCoordinator: OverlayContainerTransitionCoordinator) {
61         let dismissalContext = ConcreteOverlayContainerDismissalPolicyContext(
62             context: transitionCoordinator
63         )
64         transitionCoordinator.animate(alongsideTransition: { [weak self] context in
65             self?.dimmingView?.overlayViewControllerWillTranslate(context: context)
66         }, completion: nil)
67         let policy = makeDismissalPolicy()
68         if !presentedViewController.isBeingDismissed && policy.shouldDismiss(using: dismissalContext) {
69             presentingViewController.dismiss(animated: true, completion: nil)
70         }
71     }
72 
73     // MARK: - UIPresentationController
74 
presentationTransitionWillBeginnull75     open override func presentationTransitionWillBegin() {
76         super.presentationTransitionWillBegin()
77         startDimmingViewPresentationTransition()
78         setUpTapGesture()
79     }
80 
dismissalTransitionWillBeginnull81     open override func dismissalTransitionWillBegin() {
82         super.dismissalTransitionWillBegin()
83         startDimmingViewDismissalTransition()
84     }
85 
presentationTransitionDidEndnull86     open override func presentationTransitionDidEnd(_ completed: Bool) {
87         super.presentationTransitionDidEnd(completed)
88         guard !completed else { return }
89         dimmingView?.removeFromSuperview()
90         tapGestureRecognizerView.removeFromSuperview()
91     }
92 
93     // MARK: - Action
94 
tapGestureActionnull95     @objc private func tapGestureAction(_ sender: UITapGestureRecognizer) {
96         let shouldDismiss = sheetDelegate?.overlayContainerSheetPresentationControllerShouldDismissOnTap(self) ?? true
97         guard shouldDismiss else { return }
98         presentedViewController.dismiss(animated: true, completion: nil)
99     }
100 
101     // MARK: - Private
102 
setUpTapGesturenull103     private func setUpTapGesture() {
104         guard tapGestureRecognizerView.superview == nil,
105             dismissingTapGestureRecognizer.isEnabled else {
106                 return
107         }
108         containerView?.addSubview(tapGestureRecognizerView)
109         tapGestureRecognizerView.pinToSuperview()
110         tapGestureRecognizerView.addGestureRecognizer(dismissingTapGestureRecognizer)
111     }
112 
startDimmingViewPresentationTransitionnull113     private func startDimmingViewPresentationTransition() {
114         guard let dimmingView = dimmingView else { return }
115         if dimmingView.superview == nil {
116             containerView?.addSubview(dimmingView)
117             dimmingView.pinToSuperview()
118         }
119         containerView?.layoutIfNeeded()
120         dimmingView.presentationTransitionWillBegin()
121         presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
122             dimmingView.presentationTransitionDidBegin()
123         }, completion: nil)
124     }
125 
startDimmingViewDismissalTransitionnull126     private func startDimmingViewDismissalTransition() {
127         guard let dimmingView = dimmingView else { return }
128         dimmingView.dismissalTransitionWillBegin()
129         presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
130             dimmingView.dismissalTransitionDidBegin()
131         }, completion: nil)
132     }
133 
makeDismissalPolicynull134     private func makeDismissalPolicy() -> OverlayContainerSheetDismissalPolicy {
135         sheetDelegate?.overlayContainerSheetDismissalPolicy(for: self) ?? ThresholdOverlayContainerSheetDismissalPolicy()
136     }
137 
makeTapGestureRecognizerViewnull138     private func makeTapGestureRecognizerView() -> UIView {
139         UIView()
140     }
141 
makeTapGestureRecognizernull142     private func makeTapGestureRecognizer() -> UITapGestureRecognizer {
143         UITapGestureRecognizer(target: self, action: #selector(tapGestureAction(_:)))
144     }
145 }
146 
147 public extension OverlayContainerSheetPresentationControllerDelegate {
148 
overlayContainerSheetPresentationControllerShouldDismissOnTapnull149     func overlayContainerSheetPresentationControllerShouldDismissOnTap(_ presentationController: OverlayContainerSheetPresentationController) -> Bool {
150         true
151     }
152 
overlayContainerSheetDismissalPolicynull153     func overlayContainerSheetDismissalPolicy(for presentationController: OverlayContainerSheetPresentationController) -> OverlayContainerSheetDismissalPolicy {
154         ThresholdOverlayContainerSheetDismissalPolicy()
155     }
156 }
157