Merge branch 'master' into feature/minimize_pan_modal_changes

This commit is contained in:
Ivan Smolin 2023-07-25 15:36:33 +03:00
commit eaf349654d
5 changed files with 111 additions and 105 deletions

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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