feature/minimize_pan_modal_changes #2

Merged
ivan.smolin merged 3 commits from feature/minimize_pan_modal_changes into master 2023-07-25 15:40:34 +03:00
5 changed files with 113 additions and 107 deletions

View File

@ -8,7 +8,7 @@
Pod::Spec.new do |s| Pod::Spec.new do |s|
s.name = 'PanModal' 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.' 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. # This description is used to generate tags and improve search results.
@ -18,10 +18,10 @@ Pod::Spec.new do |s|
# * Finally, don't worry about the indent, CocoaPods strips it! # * Finally, don't worry about the indent, CocoaPods strips it!
s.description = 'PanModal is an elegant and highly customizable presentation API for constructing bottom sheet modals on iOS.' s.description = 'PanModal is an elegant and highly customizable presentation API for constructing bottom sheet modals on iOS.'
s.homepage = 'https://github.com/slackhq/PanModal' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/TIPanModal'
s.license = { :type => 'MIT', :file => 'LICENSE' } s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'slack' => 'opensource@slack.com' } s.author = { 'slack' => 'opensource@slack.com' }
s.source = { :git => 'https://github.com/slackhq/PanModal.git', :tag => s.version.to_s } s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/TIPanModal.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/slackhq' s.social_media_url = 'https://twitter.com/slackhq'
s.ios.deployment_target = '10.0' s.ios.deployment_target = '10.0'
s.swift_version = '5.0' s.swift_version = '5.0'

View File

@ -112,19 +112,15 @@ open class PanModalPresentationController: UIPresentationController {
Background view used as an overlay over the presenting view Background view used as an overlay over the presenting view
*/ */
private lazy var backgroundView: DimmedView = { private lazy var backgroundView: DimmedView = {
let view: DimmedView let view: DimmedView = presentable?.dimmedView ?? DimmedView()
let type = presentable?.dimmedViewType ?? .opaque
if let color = presentable?.panModalBackgroundColor { if let color = presentable?.panModalBackgroundColor {
view = DimmedView(presentingController: presentingViewController, dimColor: color, appearanceType: type) view.backgroundColor = color
} else {
view = DimmedView(presentingController: presentingViewController, appearanceType: type)
} }
view.didTap = { [weak self] _ in
view.didTap = { [weak self] in if self?.presentable?.allowsTapToDismiss == true {
self?.presentable?.onTapToDismiss?() self?.presentable?.onTapToDismiss?()
}
} }
return view return view
}() }()
@ -138,6 +134,16 @@ open class PanModalPresentationController: UIPresentationController {
return PanContainerView(presentedView: presentedViewController.view, frame: frame) 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 Override presented view to return the pan container wrapper
*/ */
@ -179,7 +185,6 @@ open class PanModalPresentationController: UIPresentationController {
layoutBackgroundView(in: containerView) layoutBackgroundView(in: containerView)
layoutPresentedView(in: containerView) layoutPresentedView(in: containerView)
configureScrollViewInsets() configureScrollViewInsets()
configureShadowIfNeeded()
guard let coordinator = presentedViewController.transitionCoordinator else { guard let coordinator = presentedViewController.transitionCoordinator else {
backgroundView.dimState = .max backgroundView.dimState = .max
@ -213,6 +218,7 @@ open class PanModalPresentationController: UIPresentationController {
so hiding it on view dismiss means avoiding visual bugs so hiding it on view dismiss means avoiding visual bugs
*/ */
coordinator.animate(alongsideTransition: { [weak self] _ in coordinator.animate(alongsideTransition: { [weak self] _ in
self?.dragIndicatorView.alpha = 0.0
self?.backgroundView.dimState = .off self?.backgroundView.dimState = .off
self?.presentingViewController.setNeedsStatusBarAppearanceUpdate() self?.presentingViewController.setNeedsStatusBarAppearanceUpdate()
}) })
@ -352,6 +358,10 @@ private extension PanModalPresentationController {
containerView.addSubview(presentedView) containerView.addSubview(presentedView)
containerView.addGestureRecognizer(panGestureRecognizer) containerView.addGestureRecognizer(panGestureRecognizer)
if presentable.showDragIndicator {
addDragIndicatorView(to: presentedView)
}
if presentable.shouldRoundTopCorners { if presentable.shouldRoundTopCorners {
addRoundedCorners(to: presentedView) addRoundedCorners(to: presentedView)
} }
@ -371,8 +381,10 @@ private extension PanModalPresentationController {
let adjustedSize = CGSize(width: frame.size.width, height: frame.size.height - anchoredYPosition) let adjustedSize = CGSize(width: frame.size.width, height: frame.size.height - anchoredYPosition)
let panFrame = panContainerView.frame let panFrame = panContainerView.frame
panContainerView.frame.size = frame.size 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 // if the container is already in the correct position, no need to adjust positioning
// (rotations & size changes cause positioning to be out of sync) // (rotations & size changes cause positioning to be out of sync)
let yPosition = panFrame.origin.y - panFrame.height + frame.height let yPosition = panFrame.origin.y - panFrame.height + frame.height
@ -405,6 +417,19 @@ private extension PanModalPresentationController {
backgroundView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true 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 Calculates & stores the layout anchor points & options
*/ */
@ -443,7 +468,15 @@ private extension PanModalPresentationController {
Set the appropriate contentInset as the configuration within this class Set the appropriate contentInset as the configuration within this class
offsets it 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` 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 // MARK: - Pan Gesture Event Handler
@ -514,11 +540,13 @@ private extension PanModalPresentationController {
if velocity.y < 0 { if velocity.y < 0 {
transition(to: .longForm) 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 { && presentedView.frame.minY < mediumFormYPosition {
transition(to: .mediumForm) 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 { && presentedView.frame.minY < shortFormYPosition) || allowsDragToDismiss == false {
transition(to: .shortForm) transition(to: .shortForm)
@ -532,7 +560,11 @@ private extension PanModalPresentationController {
The `containerView.bounds.height` is used to determine The `containerView.bounds.height` is used to determine
how close the presented view is to the bottom of the screen 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 { if position == longFormYPosition {
transition(to: .longForm) transition(to: .longForm)
@ -659,7 +691,7 @@ private extension PanModalPresentationController {
let maxHeight = UIScreen.main.bounds.height - longFormYPosition 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], byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: radius, height: radius)) 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 // Set path as a mask to display optional drag indicator view & rounded corners
let mask = CAShapeLayer() let mask = CAShapeLayer()
mask.path = path.cgPath mask.path = path.cgPath

View File

@ -47,13 +47,9 @@ public extension PanModalPresentable where Self: UIViewController {
return .contentHeight(scrollView.contentSize.height) return .contentHeight(scrollView.contentSize.height)
} }
var dimmedViewType: DimmedView.AppearanceType { var dimmedView: DimmedView? {
.opaque DimmedView()
} }
var presentationDetents: [ModalViewPresentationDetent] {
[]
}
var cornerRadius: CGFloat { var cornerRadius: CGFloat {
return 8.0 return 8.0

View File

@ -57,9 +57,7 @@ public protocol PanModalPresentable: AnyObject {
*/ */
var longFormHeight: PanModalHeight { get } var longFormHeight: PanModalHeight { get }
var presentationDetents: [ModalViewPresentationDetent] { get } var dimmedView: DimmedView? { get }
var dimmedViewType: DimmedView.AppearanceType { get }
/** /**
The corner radius used when `shouldRoundTopCorners` is enabled. The corner radius used when `shouldRoundTopCorners` is enabled.
@ -100,6 +98,13 @@ public protocol PanModalPresentable: AnyObject {
*/ */
var panModalBackgroundColor: UIColor { get } 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 We configure the panScrollable's scrollIndicatorInsets interally so override this value
to set custom insets. to set custom insets.
@ -132,7 +137,9 @@ public protocol PanModalPresentable: AnyObject {
Default value is true. Default value is true.
*/ */
var allowsDragToDismiss: Bool { get } var allowsDragToDismiss: Bool { get }
var onTapToDismiss: (() -> Void)? { get } var onTapToDismiss: (() -> Void)? { get }
var onDragToDismiss: (() -> Void)? { get } var onDragToDismiss: (() -> Void)? { get }
/** /**
@ -165,6 +172,13 @@ public protocol PanModalPresentable: AnyObject {
*/ */
var shouldRoundTopCorners: Bool { get } 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. 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. A dim view for use as an overlay over content you want dimmed.
*/ */
public class DimmedView: UIView { open 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
}
}
}
/** /**
Represents the possible states of the dimmed view. Represents the possible states of the dimmed view.
max, off or a percentage of dimAlpha. max, off or a percentage of dimAlpha.
*/ */
enum DimState { public enum DimState {
case max case max
case off case off
case percent(CGFloat) case percent(CGFloat)
@ -47,77 +30,52 @@ public class DimmedView: UIView {
*/ */
var dimState: DimState = .off { var dimState: DimState = .off {
didSet { didSet {
guard !appearanceType.isTransparent else { onChange(dimState: dimState)
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))
}
} }
} }
weak var presentingController: UIViewController?
var appearanceType: AppearanceType
/** /**
The closure to be executed when a tap occurs 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 // MARK: - Initializers
init(presentingController: UIViewController? = nil, init() {
dimColor: UIColor = UIColor.black.withAlphaComponent(0.7),
appearanceType: AppearanceType) {
self.presentingController = presentingController
self.appearanceType = appearanceType
super.init(frame: .zero) super.init(frame: .zero)
alpha = 0.0 alpha = 0.0
backgroundColor = dimColor
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapView))
addGestureRecognizer(tapGesture) addGestureRecognizer(tapGesture)
} }
@available(*, unavailable)
required public init?(coder aDecoder: NSCoder) { required public init?(coder aDecoder: NSCoder) {
fatalError() 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 // MARK: - Event Handlers
@objc private func didTapView() { @objc private func didTapView(sender: UITapGestureRecognizer) {
didTap?() 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 #endif