diff --git a/PanModal.podspec b/PanModal.podspec index 22b04ee..a92594f 100644 --- a/PanModal.podspec +++ b/PanModal.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'PanModal' - s.version = '1.2.7' + s.version = '1.3.0' s.summary = 'PanModal is an elegant and highly customizable presentation API for constructing bottom sheet modals on iOS.' # This description is used to generate tags and improve search results. diff --git a/PanModal/Controller/PanModalPresentationController.swift b/PanModal/Controller/PanModalPresentationController.swift index deb2a97..db5ad61 100644 --- a/PanModal/Controller/PanModalPresentationController.swift +++ b/PanModal/Controller/PanModalPresentationController.swift @@ -112,19 +112,15 @@ open class PanModalPresentationController: UIPresentationController { Background view used as an overlay over the presenting view */ private lazy var backgroundView: DimmedView = { - let view: DimmedView - let type = presentable?.dimmedViewType ?? .opaque - + let view: DimmedView = presentable?.dimmedView ?? DimmedView() if let color = presentable?.panModalBackgroundColor { - view = DimmedView(presentingController: presentingViewController, dimColor: color, appearanceType: type) - } else { - view = DimmedView(presentingController: presentingViewController, appearanceType: type) + view.backgroundColor = color } - - view.didTap = { [weak self] in - self?.presentable?.onTapToDismiss?() + view.didTap = { [weak self] _ in + if self?.presentable?.allowsTapToDismiss == true { + self?.presentable?.onTapToDismiss?() + } } - return view }() @@ -138,6 +134,16 @@ open class PanModalPresentationController: UIPresentationController { return PanContainerView(presentedView: presentedViewController.view, frame: frame) }() + /** + Drag Indicator View + */ + private lazy var dragIndicatorView: UIView = { + let view = UIView() + view.backgroundColor = presentable?.dragIndicatorBackgroundColor + view.layer.cornerRadius = Constants.dragIndicatorSize.height / 2.0 + return view + }() + /** Override presented view to return the pan container wrapper */ @@ -179,7 +185,6 @@ open class PanModalPresentationController: UIPresentationController { layoutBackgroundView(in: containerView) layoutPresentedView(in: containerView) configureScrollViewInsets() - configureShadowIfNeeded() guard let coordinator = presentedViewController.transitionCoordinator else { backgroundView.dimState = .max @@ -213,6 +218,7 @@ open class PanModalPresentationController: UIPresentationController { so hiding it on view dismiss means avoiding visual bugs */ coordinator.animate(alongsideTransition: { [weak self] _ in + self?.dragIndicatorView.alpha = 0.0 self?.backgroundView.dimState = .off self?.presentingViewController.setNeedsStatusBarAppearanceUpdate() }) @@ -352,6 +358,10 @@ private extension PanModalPresentationController { containerView.addSubview(presentedView) containerView.addGestureRecognizer(panGestureRecognizer) + if presentable.showDragIndicator { + addDragIndicatorView(to: presentedView) + } + if presentable.shouldRoundTopCorners { addRoundedCorners(to: presentedView) } @@ -371,8 +381,10 @@ private extension PanModalPresentationController { let adjustedSize = CGSize(width: frame.size.width, height: frame.size.height - anchoredYPosition) let panFrame = panContainerView.frame panContainerView.frame.size = frame.size + + let positions = [shortFormYPosition, mediumFormYPosition, longFormYPosition] - if ![shortFormYPosition, mediumFormYPosition, longFormYPosition].contains(panFrame.origin.y) { + if !positions.contains(panFrame.origin.y) { // if the container is already in the correct position, no need to adjust positioning // (rotations & size changes cause positioning to be out of sync) let yPosition = panFrame.origin.y - panFrame.height + frame.height @@ -405,6 +417,19 @@ private extension PanModalPresentationController { backgroundView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true } + /** + Adds the drag indicator view to the view hierarchy + & configures its layout constraints. + */ + func addDragIndicatorView(to view: UIView) { + view.addSubview(dragIndicatorView) + dragIndicatorView.translatesAutoresizingMaskIntoConstraints = false + dragIndicatorView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: -Constants.indicatorYOffset).isActive = true + dragIndicatorView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + dragIndicatorView.widthAnchor.constraint(equalToConstant: Constants.dragIndicatorSize.width).isActive = true + dragIndicatorView.heightAnchor.constraint(equalToConstant: Constants.dragIndicatorSize.height).isActive = true + } + /** Calculates & stores the layout anchor points & options */ @@ -443,7 +468,15 @@ private extension PanModalPresentationController { Set the appropriate contentInset as the configuration within this class offsets it */ - scrollView.contentInset.bottom = presentingViewController.bottomLayoutGuide.length + let bottomInset: CGFloat + + if #available(iOS 11.0, *) { + bottomInset = presentingViewController.view.safeAreaInsets.bottom + } else { + bottomInset = presentingViewController.bottomLayoutGuide.length + } + + scrollView.contentInset.bottom = bottomInset /** As we adjust the bounds during `handleScrollViewTopBounce` @@ -454,13 +487,6 @@ private extension PanModalPresentationController { } } - func configureShadowIfNeeded() { - let type = presentable?.dimmedViewType ?? .opaque - - if case let .transparentWithShadow(shadow) = type { - containerView?.configure(shadow: shadow) - } - } } // MARK: - Pan Gesture Event Handler @@ -514,11 +540,13 @@ private extension PanModalPresentationController { if velocity.y < 0 { transition(to: .longForm) - } else if nearest(to: presentedView.frame.minY, inValues: [mediumFormYPosition, containerView.bounds.height]) == mediumFormYPosition + } else if nearest(to: presentedView.frame.minY, + inValues: [mediumFormYPosition, containerView.bounds.height]) == mediumFormYPosition && presentedView.frame.minY < mediumFormYPosition { transition(to: .mediumForm) - } else if (nearest(to: presentedView.frame.minY, inValues: [longFormYPosition, containerView.bounds.height]) == longFormYPosition + } else if (nearest(to: presentedView.frame.minY, + inValues: [longFormYPosition, containerView.bounds.height]) == longFormYPosition && presentedView.frame.minY < shortFormYPosition) || allowsDragToDismiss == false { transition(to: .shortForm) @@ -532,7 +560,11 @@ private extension PanModalPresentationController { The `containerView.bounds.height` is used to determine how close the presented view is to the bottom of the screen */ - let position = nearest(to: presentedView.frame.minY, inValues: [containerView.bounds.height, shortFormYPosition, mediumFormYPosition, longFormYPosition]) + let position = nearest(to: presentedView.frame.minY, + inValues: [containerView.bounds.height, + shortFormYPosition, + mediumFormYPosition, + longFormYPosition]) if position == longFormYPosition { transition(to: .longForm) @@ -659,7 +691,7 @@ private extension PanModalPresentationController { let maxHeight = UIScreen.main.bounds.height - longFormYPosition - backgroundView.dimState = .percent(1 - presentedView.frame.origin.y / maxHeight) + backgroundView.dimState = .percent(1.0 - presentedView.frame.origin.y / maxHeight) } /** @@ -841,6 +873,12 @@ private extension PanModalPresentationController { byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: radius, height: radius)) + // Draw around the drag indicator view, if displayed + if presentable?.showDragIndicator == true { + let indicatorLeftEdgeXPos = view.bounds.width/2.0 - Constants.dragIndicatorSize.width/2.0 + drawAroundDragIndicator(currentPath: path, indicatorLeftEdgeXPos: indicatorLeftEdgeXPos) + } + // Set path as a mask to display optional drag indicator view & rounded corners let mask = CAShapeLayer() mask.path = path.cgPath diff --git a/PanModal/Presentable/PanModalPresentable+Defaults.swift b/PanModal/Presentable/PanModalPresentable+Defaults.swift index dea4703..3a76237 100644 --- a/PanModal/Presentable/PanModalPresentable+Defaults.swift +++ b/PanModal/Presentable/PanModalPresentable+Defaults.swift @@ -47,13 +47,9 @@ public extension PanModalPresentable where Self: UIViewController { return .contentHeight(scrollView.contentSize.height) } - var dimmedViewType: DimmedView.AppearanceType { - .opaque - } - - var presentationDetents: [ModalViewPresentationDetent] { - [] - } + var dimmedView: DimmedView? { + DimmedView() + } var cornerRadius: CGFloat { return 8.0 diff --git a/PanModal/Presentable/PanModalPresentable.swift b/PanModal/Presentable/PanModalPresentable.swift index 780be91..a8c0c62 100644 --- a/PanModal/Presentable/PanModalPresentable.swift +++ b/PanModal/Presentable/PanModalPresentable.swift @@ -57,9 +57,7 @@ public protocol PanModalPresentable: AnyObject { */ var longFormHeight: PanModalHeight { get } - var presentationDetents: [ModalViewPresentationDetent] { get } - - var dimmedViewType: DimmedView.AppearanceType { get } + var dimmedView: DimmedView? { get } /** The corner radius used when `shouldRoundTopCorners` is enabled. @@ -100,6 +98,13 @@ public protocol PanModalPresentable: AnyObject { */ var panModalBackgroundColor: UIColor { get } + /** + The drag indicator view color. + + Default value is light gray. + */ + var dragIndicatorBackgroundColor: UIColor { get } + /** We configure the panScrollable's scrollIndicatorInsets interally so override this value to set custom insets. @@ -132,7 +137,9 @@ public protocol PanModalPresentable: AnyObject { Default value is true. */ var allowsDragToDismiss: Bool { get } + var onTapToDismiss: (() -> Void)? { get } + var onDragToDismiss: (() -> Void)? { get } /** @@ -165,6 +172,13 @@ public protocol PanModalPresentable: AnyObject { */ var shouldRoundTopCorners: Bool { get } + /** + A flag to determine if a drag indicator should be shown + above the pan modal container view. + + Default value is true. + */ + var showDragIndicator: Bool { get } /** Asks the delegate if the pan modal should respond to the pan modal gesture recognizer. diff --git a/PanModal/View/DimmedView.swift b/PanModal/View/DimmedView.swift index b3a154d..e6be007 100644 --- a/PanModal/View/DimmedView.swift +++ b/PanModal/View/DimmedView.swift @@ -11,30 +11,13 @@ import UIKit /** A dim view for use as an overlay over content you want dimmed. */ -public class DimmedView: UIView { - - public enum AppearanceType { - - case transparent - case transparentWithShadow(ShadowConfigurator) - case opaque - - var isTransparent: Bool { - switch self { - case .transparent, .transparentWithShadow: - return true - - default: - return false - } - } - } +open class DimmedView: UIView { /** Represents the possible states of the dimmed view. max, off or a percentage of dimAlpha. */ - enum DimState { + public enum DimState { case max case off case percent(CGFloat) @@ -47,77 +30,52 @@ public class DimmedView: UIView { */ var dimState: DimState = .off { didSet { - guard !appearanceType.isTransparent else { - alpha = .zero - return - } - - switch dimState { - case .max: - alpha = 1.0 - case .off: - alpha = 0.0 - case .percent(let percentage): - alpha = max(0.0, min(1.0, percentage)) - } + onChange(dimState: dimState) } } - weak var presentingController: UIViewController? - var appearanceType: AppearanceType - /** The closure to be executed when a tap occurs */ - var didTap: (() -> Void)? + var didTap: ((_ recognizer: UITapGestureRecognizer) -> Void)? + + /** + Tap gesture recognizer + */ + private lazy var tapGesture: UIGestureRecognizer = { + return UITapGestureRecognizer(target: self, action: #selector(didTapView)) + }() // MARK: - Initializers - init(presentingController: UIViewController? = nil, - dimColor: UIColor = UIColor.black.withAlphaComponent(0.7), - appearanceType: AppearanceType) { - - self.presentingController = presentingController - self.appearanceType = appearanceType - + init() { super.init(frame: .zero) - alpha = 0.0 - backgroundColor = dimColor - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapView)) addGestureRecognizer(tapGesture) } - @available(*, unavailable) required public init?(coder aDecoder: NSCoder) { fatalError() } - // MARK: - Overrides - - public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if appearanceType.isTransparent { - let subviews = presentingController?.view.subviews.reversed() ?? [] - - for subview in subviews { - if let hittedView = subview.hitTest(point, with: event) { - return hittedView - } - } - } - - return super.hitTest(point, with: event) - } - - public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - !appearanceType.isTransparent - } - // MARK: - Event Handlers - @objc private func didTapView() { - didTap?() + @objc private func didTapView(sender: UITapGestureRecognizer) { + didTap?(sender) } + + // MARK: - Subclass override + + open func onChange(dimState: DimState) { + switch dimState { + case .max: + alpha = 1.0 + case .off: + alpha = 0.0 + case .percent(let percentage): + alpha = max(0.0, min(1.0, percentage)) + } + } + } #endif