Merge branch 'master' into feature/minimize_pan_modal_changes
This commit is contained in:
commit
eaf349654d
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -372,7 +382,9 @@ private extension PanModalPresentationController {
|
|||
let panFrame = panContainerView.frame
|
||||
panContainerView.frame.size = frame.size
|
||||
|
||||
if ![shortFormYPosition, mediumFormYPosition, longFormYPosition].contains(panFrame.origin.y) {
|
||||
let positions = [shortFormYPosition, mediumFormYPosition, longFormYPosition]
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue