From 83d4b7024c842bb2a0f387e32d1d042402dcd23c Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Tue, 25 Jul 2023 14:03:33 +0300 Subject: [PATCH] Add mediumForm and open DimmedView customization --- PanModal.podspec | 2 +- .../PanModalPresentationController.swift | 76 +++++++++++++------ .../PanModalPresentable+Defaults.swift | 20 +++++ .../PanModalPresentable+LayoutHelpers.swift | 6 ++ .../Presentable/PanModalPresentable.swift | 7 ++ PanModal/View/DimmedView.swift | 35 +++++---- 6 files changed, 105 insertions(+), 41 deletions(-) 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 11fb2a6..db5ad61 100644 --- a/PanModal/Controller/PanModalPresentationController.swift +++ b/PanModal/Controller/PanModalPresentationController.swift @@ -30,6 +30,7 @@ open class PanModalPresentationController: UIPresentationController { */ public enum PresentationState { case shortForm + case mediumForm case longForm } @@ -79,11 +80,17 @@ open class PanModalPresentationController: UIPresentationController { */ private var shortFormYPosition: CGFloat = 0 + private var mediumFormYPosition: CGFloat = 0 + /** The y value for the long form presentation state */ private var longFormYPosition: CGFloat = 0 + private var allowsDragToDismiss: Bool { + presentable?.onDragToDismiss != nil + } + /** Determine anchored Y postion based on the `anchorModalToLongForm` flag */ @@ -105,15 +112,13 @@ open class PanModalPresentationController: UIPresentationController { Background view used as an overlay over the presenting view */ private lazy var backgroundView: DimmedView = { - let view: DimmedView + let view: DimmedView = presentable?.dimmedView ?? DimmedView() if let color = presentable?.panModalBackgroundColor { - view = DimmedView(dimColor: color) - } else { - view = DimmedView() + view.backgroundColor = color } view.didTap = { [weak self] _ in if self?.presentable?.allowsTapToDismiss == true { - self?.presentedViewController.dismiss(animated: true) + self?.presentable?.onTapToDismiss?() } } return view @@ -187,7 +192,9 @@ open class PanModalPresentationController: UIPresentationController { } coordinator.animate(alongsideTransition: { [weak self] _ in - self?.backgroundView.dimState = .max + if let yPos = self?.shortFormYPosition { + self?.adjust(toYPosition: yPos) + } self?.presentedViewController.setNeedsStatusBarAppearanceUpdate() }) } @@ -262,6 +269,10 @@ public extension PanModalPresentationController { switch state { case .shortForm: snap(toYPosition: shortFormYPosition) + + case .mediumForm: + snap(toYPosition: mediumFormYPosition) + case .longForm: snap(toYPosition: longFormYPosition) } @@ -370,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, 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 @@ -426,6 +439,7 @@ private extension PanModalPresentationController { else { return } shortFormYPosition = layoutPresentable.shortFormYPos + mediumFormYPosition = layoutPresentable.mediumFormYPos longFormYPosition = layoutPresentable.longFormYPos anchorModalToLongForm = layoutPresentable.anchorModalToLongForm extendsPanScrolling = layoutPresentable.allowsExtendedPanScrolling @@ -454,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` @@ -518,12 +540,18 @@ private extension PanModalPresentationController { if velocity.y < 0 { transition(to: .longForm) - } else if (nearest(to: presentedView.frame.minY, inValues: [longFormYPosition, containerView.bounds.height]) == longFormYPosition - && presentedView.frame.minY < shortFormYPosition) || presentable?.allowsDragToDismiss == false { + } 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 + && presentedView.frame.minY < shortFormYPosition) || allowsDragToDismiss == false { transition(to: .shortForm) } else { - presentedViewController.dismiss(animated: true) + presentable?.onDragToDismiss?() } } else { @@ -532,16 +560,23 @@ 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, longFormYPosition]) + let position = nearest(to: presentedView.frame.minY, + inValues: [containerView.bounds.height, + shortFormYPosition, + mediumFormYPosition, + longFormYPosition]) if position == longFormYPosition { transition(to: .longForm) + + } else if position == mediumFormYPosition { + transition(to: .mediumForm) - } else if position == shortFormYPosition || presentable?.allowsDragToDismiss == false { + } else if position == shortFormYPosition || allowsDragToDismiss == false { transition(to: .shortForm) } else { - presentedViewController.dismiss(animated: true) + presentable?.onDragToDismiss?() } } } @@ -653,19 +688,10 @@ private extension PanModalPresentationController { */ func adjust(toYPosition yPos: CGFloat) { presentedView.frame.origin.y = max(yPos, anchoredYPosition) - - guard presentedView.frame.origin.y > shortFormYPosition else { - backgroundView.dimState = .max - return - } - let yDisplacementFromShortForm = presentedView.frame.origin.y - shortFormYPosition + let maxHeight = UIScreen.main.bounds.height - longFormYPosition - /** - Once presentedView is translated below shortForm, calculate yPos relative to bottom of screen - and apply percentage to backgroundView alpha - */ - backgroundView.dimState = .percent(1.0 - (yDisplacementFromShortForm / presentedView.frame.height)) + backgroundView.dimState = .percent(1.0 - presentedView.frame.origin.y / maxHeight) } /** diff --git a/PanModal/Presentable/PanModalPresentable+Defaults.swift b/PanModal/Presentable/PanModalPresentable+Defaults.swift index 76a0679..4b31e06 100644 --- a/PanModal/Presentable/PanModalPresentable+Defaults.swift +++ b/PanModal/Presentable/PanModalPresentable+Defaults.swift @@ -13,6 +13,18 @@ import UIKit */ public extension PanModalPresentable where Self: UIViewController { + var onTapToDismiss: (() -> Void)? { + { [weak self] in + self?.dismiss(animated: true) + } + } + + var onDragToDismiss: (() -> Void)? { + { [weak self] in + self?.dismiss(animated: true) + } + } + var topOffset: CGFloat { return topLayoutOffset + 21.0 } @@ -21,6 +33,10 @@ public extension PanModalPresentable where Self: UIViewController { return longFormHeight } + var mediumFormHeight: PanModalHeight { + longFormHeight + } + var longFormHeight: PanModalHeight { guard let scrollView = panScrollable @@ -31,6 +47,10 @@ public extension PanModalPresentable where Self: UIViewController { return .contentHeight(scrollView.contentSize.height) } + var dimmedView: DimmedView? { + DimmedView() + } + var cornerRadius: CGFloat { return 8.0 } diff --git a/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift b/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift index ff4805c..0e93abe 100644 --- a/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift +++ b/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift @@ -63,6 +63,12 @@ extension PanModalPresentable where Self: UIViewController { return max(shortFormYPos, longFormYPos) } + var mediumFormYPos: CGFloat { + let mediumFormYPos = topMargin(from: mediumFormHeight) + + return max(mediumFormYPos, longFormYPos) + } + /** Returns the long form Y position diff --git a/PanModal/Presentable/PanModalPresentable.swift b/PanModal/Presentable/PanModalPresentable.swift index 76c1501..a8c0c62 100644 --- a/PanModal/Presentable/PanModalPresentable.swift +++ b/PanModal/Presentable/PanModalPresentable.swift @@ -45,6 +45,8 @@ public protocol PanModalPresentable: AnyObject { */ var shortFormHeight: PanModalHeight { get } + var mediumFormHeight: PanModalHeight { get } + /** The height of the pan modal container view when in the longForm presentation state. @@ -55,6 +57,7 @@ public protocol PanModalPresentable: AnyObject { */ var longFormHeight: PanModalHeight { get } + var dimmedView: DimmedView? { get } /** The corner radius used when `shouldRoundTopCorners` is enabled. @@ -135,6 +138,10 @@ public protocol PanModalPresentable: AnyObject { */ var allowsDragToDismiss: Bool { get } + var onTapToDismiss: (() -> Void)? { get } + + var onDragToDismiss: (() -> Void)? { get } + /** A flag to determine if dismissal should be initiated when tapping on the dimmed background view. diff --git a/PanModal/View/DimmedView.swift b/PanModal/View/DimmedView.swift index 6fdd1d8..e6be007 100644 --- a/PanModal/View/DimmedView.swift +++ b/PanModal/View/DimmedView.swift @@ -11,13 +11,13 @@ import UIKit /** A dim view for use as an overlay over content you want dimmed. */ -public class DimmedView: UIView { +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) @@ -30,21 +30,14 @@ public class DimmedView: UIView { */ var dimState: DimState = .off { didSet { - 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) } } /** The closure to be executed when a tap occurs */ - var didTap: ((_ recognizer: UIGestureRecognizer) -> Void)? + var didTap: ((_ recognizer: UITapGestureRecognizer) -> Void)? /** Tap gesture recognizer @@ -55,10 +48,9 @@ public class DimmedView: UIView { // MARK: - Initializers - init(dimColor: UIColor = UIColor.black.withAlphaComponent(0.7)) { + init() { super.init(frame: .zero) alpha = 0.0 - backgroundColor = dimColor addGestureRecognizer(tapGesture) } @@ -68,8 +60,21 @@ public class DimmedView: UIView { // MARK: - Event Handlers - @objc private func didTapView() { - didTap?(tapGesture) + @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)) + } } }