Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a792f46e3d | |||
| d6b4ba3d05 | |||
| 5c1d8c49a7 | |||
| af264ebb0d | |||
| 9d73db3919 | |||
| 6b1edd1dfb | |||
| 9134d20032 | |||
| 0c6f9ff040 | |||
| 12ac36646f | |||
| f9fedcb597 | |||
| 4206308afd | |||
| 0234d82979 | |||
| b0fb9e7eed | |||
| a9edeb1d71 | |||
| b8f7e613ec | |||
| 8a662e829b | |||
| 0b2152f878 | |||
| 13251e1db7 | |||
| da4c94e47e | |||
| 0d999396a5 | |||
| 8e39823698 | |||
| 457ebaed94 | |||
| 32e4a6fa24 | |||
| 2736468072 | |||
| c76ae09bc9 | |||
| 12d6380d07 | |||
| 77c9f6f806 | |||
| 964d444ff6 | |||
| 9456969502 | |||
| 0750bd980e | |||
| 056351af4b | |||
| dab02d34c0 | |||
| 96591ef52c |
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'PanModal'
|
||||
s.version = '1.2'
|
||||
s.version = '1.2.4'
|
||||
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.
|
||||
|
||||
@@ -16,18 +16,22 @@ struct PanModalAnimator {
|
||||
Constant Animation Properties
|
||||
*/
|
||||
struct Constants {
|
||||
static let transitionDuration: TimeInterval = 0.5
|
||||
static let defaultTransitionDuration: TimeInterval = 0.5
|
||||
}
|
||||
|
||||
static func animate(_ animations: @escaping PanModalPresentable.AnimationBlockType,
|
||||
config: PanModalPresentable?,
|
||||
_ completion: PanModalPresentable.AnimationCompletionType? = nil) {
|
||||
|
||||
UIView.animate(withDuration: Constants.transitionDuration,
|
||||
let transitionDuration = config?.transitionDuration ?? Constants.defaultTransitionDuration
|
||||
let springDamping = config?.springDamping ?? 1.0
|
||||
let animationOptions = config?.transitionAnimationOptions ?? []
|
||||
|
||||
UIView.animate(withDuration: transitionDuration,
|
||||
delay: 0,
|
||||
usingSpringWithDamping: config?.springDamping ?? 1.0,
|
||||
usingSpringWithDamping: springDamping,
|
||||
initialSpringVelocity: 0,
|
||||
options: [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState],
|
||||
options: animationOptions,
|
||||
animations: animations,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
@@ -63,11 +63,16 @@ public class PanModalPresentationAnimator: NSObject {
|
||||
*/
|
||||
private func animatePresentation(transitionContext: UIViewControllerContextTransitioning) {
|
||||
|
||||
guard let toVC = transitionContext.viewController(forKey: .to)
|
||||
guard
|
||||
let toVC = transitionContext.viewController(forKey: .to),
|
||||
let fromVC = transitionContext.viewController(forKey: .from)
|
||||
else { return }
|
||||
|
||||
let presentable = toVC as? PanModalPresentable.LayoutType
|
||||
let presentable = panModalLayoutType(from: transitionContext)
|
||||
|
||||
// Calls viewWillAppear and viewWillDisappear
|
||||
fromVC.beginAppearanceTransition(false, animated: true)
|
||||
|
||||
// Presents the view in shortForm position, initially
|
||||
let yPos: CGFloat = presentable?.shortFormYPos ?? 0.0
|
||||
|
||||
@@ -86,6 +91,8 @@ public class PanModalPresentationAnimator: NSObject {
|
||||
PanModalAnimator.animate({
|
||||
panView.frame.origin.y = yPos
|
||||
}, config: presentable) { [weak self] didComplete in
|
||||
// Calls viewDidAppear and viewDidDisappear
|
||||
fromVC.endAppearanceTransition()
|
||||
transitionContext.completeTransition(didComplete)
|
||||
self?.feedbackGenerator = nil
|
||||
}
|
||||
@@ -96,20 +103,39 @@ public class PanModalPresentationAnimator: NSObject {
|
||||
*/
|
||||
private func animateDismissal(transitionContext: UIViewControllerContextTransitioning) {
|
||||
|
||||
guard let fromVC = transitionContext.viewController(forKey: .from)
|
||||
guard
|
||||
let toVC = transitionContext.viewController(forKey: .to),
|
||||
let fromVC = transitionContext.viewController(forKey: .from)
|
||||
else { return }
|
||||
|
||||
let presentable = fromVC as? PanModalPresentable.LayoutType
|
||||
// Calls viewWillAppear and viewWillDisappear
|
||||
toVC.beginAppearanceTransition(true, animated: true)
|
||||
|
||||
let presentable = panModalLayoutType(from: transitionContext)
|
||||
let panView: UIView = transitionContext.containerView.panContainerView ?? fromVC.view
|
||||
|
||||
PanModalAnimator.animate({
|
||||
panView.frame.origin.y = transitionContext.containerView.frame.height
|
||||
}, config: presentable) { didComplete in
|
||||
fromVC.view.removeFromSuperview()
|
||||
// Calls viewDidAppear and viewDidDisappear
|
||||
toVC.endAppearanceTransition()
|
||||
transitionContext.completeTransition(didComplete)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Extracts the PanModal from the transition context, if it exists
|
||||
*/
|
||||
private func panModalLayoutType(from context: UIViewControllerContextTransitioning) -> PanModalPresentable.LayoutType? {
|
||||
switch transitionStyle {
|
||||
case .presentation:
|
||||
return context.viewController(forKey: .to) as? PanModalPresentable.LayoutType
|
||||
case .dismissal:
|
||||
return context.viewController(forKey: .from) as? PanModalPresentable.LayoutType
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UIViewControllerAnimatedTransitioning Delegate
|
||||
@@ -120,11 +146,17 @@ extension PanModalPresentationAnimator: UIViewControllerAnimatedTransitioning {
|
||||
Returns the transition duration
|
||||
*/
|
||||
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
return PanModalAnimator.Constants.transitionDuration
|
||||
|
||||
guard
|
||||
let context = transitionContext,
|
||||
let presentable = panModalLayoutType(from: context)
|
||||
else { return PanModalAnimator.Constants.defaultTransitionDuration }
|
||||
|
||||
return presentable.transitionDuration
|
||||
}
|
||||
|
||||
/**
|
||||
Perfroms the appropriate animation based on the transition style
|
||||
Performs the appropriate animation based on the transition style
|
||||
*/
|
||||
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
switch transitionStyle {
|
||||
|
||||
@@ -22,7 +22,7 @@ import UIKit
|
||||
By conforming to the PanModalPresentable protocol & overriding values
|
||||
the presented view can define its layout configuration & presentation.
|
||||
*/
|
||||
public class PanModalPresentationController: UIPresentationController {
|
||||
open class PanModalPresentationController: UIPresentationController {
|
||||
|
||||
/**
|
||||
Enum representing the possible presentation states
|
||||
@@ -105,13 +105,15 @@ public class PanModalPresentationController: UIPresentationController {
|
||||
*/
|
||||
private lazy var backgroundView: DimmedView = {
|
||||
let view: DimmedView
|
||||
if let alpha = presentable?.backgroundAlpha {
|
||||
view = DimmedView(dimAlpha: alpha)
|
||||
if let color = presentable?.panModalBackgroundColor {
|
||||
view = DimmedView(dimColor: color)
|
||||
} else {
|
||||
view = DimmedView()
|
||||
}
|
||||
view.didTap = { [weak self] _ in
|
||||
self?.dismissPresentedViewController()
|
||||
if self?.presentable?.allowsTapToDismiss == true {
|
||||
self?.dismissPresentedViewController()
|
||||
}
|
||||
}
|
||||
return view
|
||||
}()
|
||||
@@ -131,7 +133,7 @@ public class PanModalPresentationController: UIPresentationController {
|
||||
*/
|
||||
private lazy var dragIndicatorView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .lightGray
|
||||
view.backgroundColor = presentable?.dragIndicatorBackgroundColor
|
||||
view.layer.cornerRadius = Constants.dragIndicatorSize.height / 2.0
|
||||
return view
|
||||
}()
|
||||
@@ -198,7 +200,7 @@ public class PanModalPresentationController: UIPresentationController {
|
||||
|
||||
/**
|
||||
Drag indicator is drawn outside of view bounds
|
||||
so hiding it on view dismiss means avoids visual bugs
|
||||
so hiding it on view dismiss means avoiding visual bugs
|
||||
*/
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
self?.dragIndicatorView.alpha = 0.0
|
||||
@@ -213,6 +215,25 @@ public class PanModalPresentationController: UIPresentationController {
|
||||
backgroundView.removeFromSuperview()
|
||||
}
|
||||
|
||||
/**
|
||||
Update presented view size in response to size class changes
|
||||
*/
|
||||
override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
guard
|
||||
let self = self,
|
||||
let presentable = self.presentable
|
||||
else { return }
|
||||
|
||||
self.adjustPresentedViewFrame()
|
||||
if presentable.shouldRoundTopCorners {
|
||||
self.addRoundedCorners(to: self.presentedView)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
@@ -220,10 +241,10 @@ public class PanModalPresentationController: UIPresentationController {
|
||||
public extension PanModalPresentationController {
|
||||
|
||||
/**
|
||||
Tranisition the PanModalPresentationController
|
||||
Transition the PanModalPresentationController
|
||||
to the given presentation state
|
||||
*/
|
||||
public func transition(to state: PresentationState) {
|
||||
func transition(to state: PresentationState) {
|
||||
|
||||
guard presentable?.shouldTransition(to: state) == true
|
||||
else { return }
|
||||
@@ -239,46 +260,38 @@ public extension PanModalPresentationController {
|
||||
}
|
||||
|
||||
/**
|
||||
Set the content offset of the scroll view
|
||||
Operations on the scroll view, such as content height changes,
|
||||
or when inserting/deleting rows can cause the pan modal to jump,
|
||||
caused by the pan modal responding to content offset changes.
|
||||
|
||||
Due to content offset observation, its not possible to programmatically
|
||||
set the content offset directly on the scroll view while in the short form.
|
||||
|
||||
This method pauses the content offset KVO, performs the content offset chnage
|
||||
and then resumes content offset observation.
|
||||
To avoid this, you can call this method to perform scroll view updates,
|
||||
with scroll observation temporarily disabled.
|
||||
*/
|
||||
public func setContentOffset(offset: CGPoint) {
|
||||
func performUpdates(_ updates: () -> Void) {
|
||||
|
||||
guard let scrollView = presentable?.panScrollable
|
||||
else { return }
|
||||
|
||||
/**
|
||||
Invalidate scroll view observer
|
||||
to prevent its overriding the content offset change
|
||||
*/
|
||||
// Pause scroll observer
|
||||
scrollObserver?.invalidate()
|
||||
scrollObserver = nil
|
||||
|
||||
/**
|
||||
Set scroll view offset & track scrolling
|
||||
*/
|
||||
scrollView.setContentOffset(offset, animated:false)
|
||||
trackScrolling(scrollView)
|
||||
// Perform updates
|
||||
updates()
|
||||
|
||||
/**
|
||||
Add the scroll view observer
|
||||
*/
|
||||
// Resume scroll observer
|
||||
trackScrolling(scrollView)
|
||||
observe(scrollView: scrollView)
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the PanModalPresentationController layout
|
||||
based on values in the PanModalPresentabls
|
||||
based on values in the PanModalPresentable
|
||||
|
||||
- Note: This should be called whenever any
|
||||
pan modal presentable value changes after the initial presentation
|
||||
*/
|
||||
public func setNeedsLayoutUpdate() {
|
||||
func setNeedsLayoutUpdate() {
|
||||
configureViewLayout()
|
||||
adjustPresentedViewFrame()
|
||||
observe(scrollView: presentable?.panScrollable)
|
||||
@@ -342,9 +355,21 @@ private extension PanModalPresentationController {
|
||||
Reduce height of presentedView so that it sits at the bottom of the screen
|
||||
*/
|
||||
func adjustPresentedViewFrame() {
|
||||
let frame = containerView?.frame ?? .zero
|
||||
let size = CGSize(width: frame.size.width, height: frame.height - anchoredYPosition)
|
||||
presentedViewController.view.frame = CGRect(origin: .zero, size: size)
|
||||
|
||||
guard let frame = containerView?.frame
|
||||
else { return }
|
||||
|
||||
let adjustedSize = CGSize(width: frame.size.width, height: frame.size.height - anchoredYPosition)
|
||||
let panFrame = panContainerView.frame
|
||||
panContainerView.frame.size = frame.size
|
||||
|
||||
if ![shortFormYPosition, longFormYPosition].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)
|
||||
adjust(toYPosition: panFrame.origin.y - panFrame.height + frame.height)
|
||||
}
|
||||
panContainerView.frame.origin.x = frame.origin.x
|
||||
presentedViewController.view.frame = CGRect(origin: .zero, size: adjustedSize)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,7 +409,7 @@ private extension PanModalPresentationController {
|
||||
}
|
||||
|
||||
/**
|
||||
Caluclates & stores the layout anchor points & options
|
||||
Calculates & stores the layout anchor points & options
|
||||
*/
|
||||
func configureViewLayout() {
|
||||
|
||||
@@ -443,7 +468,7 @@ private extension PanModalPresentationController {
|
||||
@objc func didPanOnPresentedView(_ recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
guard
|
||||
shouldRespond(to: panGestureRecognizer),
|
||||
shouldRespond(to: recognizer),
|
||||
let containerView = containerView
|
||||
else {
|
||||
recognizer.setTranslation(.zero, in: recognizer.view)
|
||||
@@ -484,7 +509,7 @@ private extension PanModalPresentationController {
|
||||
if velocity.y < 0 {
|
||||
transition(to: .longForm)
|
||||
|
||||
} else if (nearestDistance(to: presentedView.frame.minY, inDistances: [longFormYPosition, containerView.bounds.height]) == longFormYPosition
|
||||
} else if (nearest(to: presentedView.frame.minY, inValues: [longFormYPosition, containerView.bounds.height]) == longFormYPosition
|
||||
&& presentedView.frame.minY < shortFormYPosition) || presentable?.allowsDragToDismiss == false {
|
||||
transition(to: .shortForm)
|
||||
|
||||
@@ -498,7 +523,7 @@ 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 = nearestDistance(to: presentedView.frame.minY, inDistances: [containerView.bounds.height, shortFormYPosition, longFormYPosition])
|
||||
let position = nearest(to: presentedView.frame.minY, inValues: [containerView.bounds.height, shortFormYPosition, longFormYPosition])
|
||||
|
||||
if position == longFormYPosition {
|
||||
transition(to: .longForm)
|
||||
@@ -635,16 +660,16 @@ private extension PanModalPresentationController {
|
||||
}
|
||||
|
||||
/**
|
||||
Finds the nearest distance to a given position out of a given array of distance values
|
||||
Finds the nearest value to a given number out of a given array of float values
|
||||
|
||||
- Parameters:
|
||||
- position: reference postion we are trying to find the closest distance to
|
||||
- distances: array of positions we would like to compare against
|
||||
- number: reference float we are trying to find the closest value to
|
||||
- values: array of floats we would like to compare against
|
||||
*/
|
||||
func nearestDistance(to position: CGFloat, inDistances distances: [CGFloat]) -> CGFloat {
|
||||
guard let nearestDistance = distances.min(by: { abs(position - $0) < abs(position - $1) })
|
||||
else { return position }
|
||||
return nearestDistance
|
||||
func nearest(to number: CGFloat, inValues values: [CGFloat]) -> CGFloat {
|
||||
guard let nearestVal = values.min(by: { abs(number - $0) < abs(number - $1) })
|
||||
else { return number }
|
||||
return nearestVal
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -652,7 +677,9 @@ private extension PanModalPresentationController {
|
||||
*/
|
||||
func dismissPresentedViewController() {
|
||||
presentable?.panModalWillDismiss()
|
||||
presentedViewController.dismiss(animated: true, completion: nil)
|
||||
presentedViewController.dismiss(animated: true) { [weak self] in
|
||||
self?.presentable?.panModalDidDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,7 +785,7 @@ private extension PanModalPresentationController {
|
||||
*/
|
||||
func handleScrollViewTopBounce(scrollView: UIScrollView, change: NSKeyValueObservedChange<CGPoint>) {
|
||||
|
||||
guard let oldYValue = change.oldValue?.y
|
||||
guard let oldYValue = change.oldValue?.y, scrollView.isDecelerating
|
||||
else { return }
|
||||
|
||||
let yOffset = scrollView.contentOffset.y
|
||||
@@ -798,11 +825,11 @@ extension PanModalPresentationController: UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
/**
|
||||
Allow simultaneous gesture recognizers only when the other gesture recognizer
|
||||
is a pan gesture recognizer
|
||||
Allow simultaneous gesture recognizers only when the other gesture recognizer's view
|
||||
is the pan scrollable view
|
||||
*/
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
|
||||
return otherGestureRecognizer.view == presentable?.panScrollable
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ extension PanModalPresentationDelegate: UIAdaptivePresentationControllerDelegate
|
||||
Dismisses the presented view controller
|
||||
*/
|
||||
public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
controller.presentedViewController.dismiss(animated: false, completion: nil)
|
||||
return .none
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,20 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
return 0.8
|
||||
}
|
||||
|
||||
var backgroundAlpha: CGFloat {
|
||||
return 0.7
|
||||
var transitionDuration: Double {
|
||||
return PanModalAnimator.Constants.defaultTransitionDuration
|
||||
}
|
||||
|
||||
var transitionAnimationOptions: UIView.AnimationOptions {
|
||||
return [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState]
|
||||
}
|
||||
|
||||
var panModalBackgroundColor: UIColor {
|
||||
return UIColor.black.withAlphaComponent(0.7)
|
||||
}
|
||||
|
||||
var dragIndicatorBackgroundColor: UIColor {
|
||||
return UIColor.lightGray
|
||||
}
|
||||
|
||||
var scrollIndicatorInsets: UIEdgeInsets {
|
||||
@@ -64,6 +76,10 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
return true
|
||||
}
|
||||
|
||||
var allowsTapToDismiss: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var isUserInteractionEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
@@ -104,4 +120,7 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
|
||||
}
|
||||
|
||||
func panModalDidDismiss() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ extension PanModalPresentable where Self: UIViewController {
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the short form Y postion
|
||||
Returns the short form Y position
|
||||
|
||||
- Note: If voiceover is on, the `longFormYPos` is returned.
|
||||
We do not support short form when voiceover is on as it would make it difficult for user to navigate.
|
||||
@@ -55,7 +55,7 @@ extension PanModalPresentable where Self: UIViewController {
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the long form Y postion
|
||||
Returns the long form Y position
|
||||
|
||||
- Note: We cap this value to the max possible height
|
||||
to ensure content is not rendered outside of the view bounds
|
||||
|
||||
@@ -29,16 +29,6 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
presentedVC?.transition(to: state)
|
||||
}
|
||||
|
||||
/**
|
||||
Programmatically set the content offset of the pan scrollable.
|
||||
|
||||
This is required to use while in the short form presentation state,
|
||||
as due to content offset observation, setting the content offset directly would fail
|
||||
*/
|
||||
func panModalSetContentOffset(offset: CGPoint) {
|
||||
presentedVC?.setContentOffset(offset: offset)
|
||||
}
|
||||
|
||||
/**
|
||||
A function wrapper over the `setNeedsLayoutUpdate()`
|
||||
function in the PanModalPresentationController.
|
||||
@@ -49,6 +39,16 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
presentedVC?.setNeedsLayoutUpdate()
|
||||
}
|
||||
|
||||
/**
|
||||
Operations on the scroll view, such as content height changes, or when inserting/deleting rows can cause the pan modal to jump,
|
||||
caused by the pan modal responding to content offset changes.
|
||||
|
||||
To avoid this, you can call this method to perform scroll view updates, with scroll observation temporarily disabled.
|
||||
*/
|
||||
func panModalPerformUpdates(_ updates: () -> Void) {
|
||||
presentedVC?.performUpdates(updates)
|
||||
}
|
||||
|
||||
/**
|
||||
A function wrapper over the animate function in PanModalAnimator.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import UIKit
|
||||
}
|
||||
```
|
||||
*/
|
||||
public protocol PanModalPresentable {
|
||||
public protocol PanModalPresentable: AnyObject {
|
||||
|
||||
/**
|
||||
The scroll view embedded in the view controller.
|
||||
@@ -70,13 +70,36 @@ public protocol PanModalPresentable {
|
||||
var springDamping: CGFloat { get }
|
||||
|
||||
/**
|
||||
The background view alpha.
|
||||
The transitionDuration value is used to set the speed of animation during a transition,
|
||||
including initial presentation.
|
||||
|
||||
Default value is 0.5.
|
||||
*/
|
||||
var transitionDuration: Double { get }
|
||||
|
||||
/**
|
||||
The animation options used when performing animations on the PanModal, utilized mostly
|
||||
during a transition.
|
||||
|
||||
Default value is [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState].
|
||||
*/
|
||||
var transitionAnimationOptions: UIView.AnimationOptions { get }
|
||||
|
||||
/**
|
||||
The background view color.
|
||||
|
||||
- Note: This is only utilized at the very start of the transition.
|
||||
|
||||
Default Value is 0.7.
|
||||
*/
|
||||
var backgroundAlpha: CGFloat { get }
|
||||
Default Value is black with alpha component 0.7.
|
||||
*/
|
||||
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
|
||||
@@ -111,6 +134,13 @@ public protocol PanModalPresentable {
|
||||
*/
|
||||
var allowsDragToDismiss: Bool { get }
|
||||
|
||||
/**
|
||||
A flag to determine if dismissal should be initiated when tapping on the dimmed background view.
|
||||
|
||||
Default value is true.
|
||||
*/
|
||||
var allowsTapToDismiss: Bool { get }
|
||||
|
||||
/**
|
||||
A flag to toggle user interactions on the container view.
|
||||
|
||||
@@ -196,4 +226,10 @@ public protocol PanModalPresentable {
|
||||
*/
|
||||
func panModalWillDismiss()
|
||||
|
||||
/**
|
||||
Notifies the delegate after the pan modal is dismissed.
|
||||
|
||||
Default value is an empty implementation.
|
||||
*/
|
||||
func panModalDidDismiss()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import UIKit
|
||||
sourceRect: .zero)
|
||||
```
|
||||
*/
|
||||
public protocol PanModalPresenter {
|
||||
protocol PanModalPresenter: AnyObject {
|
||||
|
||||
/**
|
||||
A flag that returns true if the current presented view controller
|
||||
|
||||
@@ -31,12 +31,11 @@ public class DimmedView: UIView {
|
||||
didSet {
|
||||
switch dimState {
|
||||
case .max:
|
||||
alpha = dimAlpha
|
||||
alpha = 1.0
|
||||
case .off:
|
||||
alpha = 0.0
|
||||
case .percent(let percentage):
|
||||
let val = max(0.0, min(1.0, percentage))
|
||||
alpha = dimAlpha * val
|
||||
alpha = max(0.0, min(1.0, percentage))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,15 +52,12 @@ public class DimmedView: UIView {
|
||||
return UITapGestureRecognizer(target: self, action: #selector(didTapView))
|
||||
}()
|
||||
|
||||
private let dimAlpha: CGFloat
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
init(dimAlpha: CGFloat = 0.7) {
|
||||
self.dimAlpha = dimAlpha
|
||||
init(dimColor: UIColor = UIColor.black.withAlphaComponent(0.7)) {
|
||||
super.init(frame: .zero)
|
||||
alpha = 0.0
|
||||
backgroundColor = .black
|
||||
backgroundColor = dimColor
|
||||
addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,9 @@ class PanContainerView: UIView {
|
||||
addSubview(presentedView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError()
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,7 +34,9 @@ extension UIView {
|
||||
from the view hierachy
|
||||
*/
|
||||
var panContainerView: PanContainerView? {
|
||||
return subviews.compactMap({ $0 as? PanContainerView }).first
|
||||
return subviews.first(where: { view -> Bool in
|
||||
view is PanContainerView
|
||||
}) as? PanContainerView
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
943904ED2226366700859537 /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943904EC2226366700859537 /* AlertViewController.swift */; };
|
||||
943904EF2226383700859537 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943904EE2226383700859537 /* NavigationController.swift */; };
|
||||
943904F32226484F00859537 /* UserGroupStackedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943904F22226484F00859537 /* UserGroupStackedViewController.swift */; };
|
||||
944EBA2E227BB7F400C4C97B /* FullScreenNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944EBA2D227BB7F400C4C97B /* FullScreenNavController.swift */; };
|
||||
94795C9B21F0335D008045A0 /* PanModalPresentationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94795C9A21F0335D008045A0 /* PanModalPresentationDelegate.swift */; };
|
||||
94795C9D21F03368008045A0 /* PanContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94795C9C21F03368008045A0 /* PanContainerView.swift */; };
|
||||
DC13905E216D90D5007A3E64 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC13905D216D90D5007A3E64 /* Assets.xcassets */; };
|
||||
@@ -112,6 +113,7 @@
|
||||
943904EC2226366700859537 /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = "<group>"; };
|
||||
943904EE2226383700859537 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; };
|
||||
943904F22226484F00859537 /* UserGroupStackedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGroupStackedViewController.swift; sourceTree = "<group>"; };
|
||||
944EBA2D227BB7F400C4C97B /* FullScreenNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenNavController.swift; sourceTree = "<group>"; };
|
||||
94795C9A21F0335D008045A0 /* PanModalPresentationDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanModalPresentationDelegate.swift; sourceTree = "<group>"; };
|
||||
94795C9C21F03368008045A0 /* PanContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanContainerView.swift; sourceTree = "<group>"; };
|
||||
DC13905D216D90D5007A3E64 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@@ -238,6 +240,22 @@
|
||||
path = Presenter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
944EBA2B227BB7D900C4C97B /* Basic */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
943904EA2226354100859537 /* BasicViewController.swift */,
|
||||
);
|
||||
path = Basic;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
944EBA2C227BB7E100C4C97B /* Full Screen */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
944EBA2D227BB7F400C4C97B /* FullScreenNavController.swift */,
|
||||
);
|
||||
path = "Full Screen";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DC13905F216D93AB007A3E64 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -310,7 +328,8 @@
|
||||
DC139079216D9AAA007A3E64 /* View Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
943904EA2226354100859537 /* BasicViewController.swift */,
|
||||
944EBA2B227BB7D900C4C97B /* Basic */,
|
||||
944EBA2C227BB7E100C4C97B /* Full Screen */,
|
||||
DC3B2EBB222A5882000C8A4A /* Alert */,
|
||||
DC3B2EBC222A5893000C8A4A /* Alert (Transient) */,
|
||||
743CB2AB222661EA00665A55 /* User Groups (Stacked) */,
|
||||
@@ -567,6 +586,7 @@
|
||||
DC139075216D9458007A3E64 /* DimmedView.swift in Sources */,
|
||||
743CABD322265F2E00634A5A /* ProfileViewController.swift in Sources */,
|
||||
DC139070216D9458007A3E64 /* PanModalPresentationAnimator.swift in Sources */,
|
||||
944EBA2E227BB7F400C4C97B /* FullScreenNavController.swift in Sources */,
|
||||
DCA741AE216D90410021F2F2 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
|
||||
### PanModal is an elegant and highly customizable presentation API for constructing bottom sheet modals on iOS.
|
||||
|
||||
Read our <a href="https://slack.engineering/panmodal-better-support-for-thumb-accessibility-on-slack-mobile-52b2a7596031" target="_blank">blog</a> on how Slack is getting more :thumbsup: with PanModal
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/slackhq/PanModal/raw/master/Screenshots/panModal.gif" width="30%" height="30%" alt="Screenshot Preview" />
|
||||
</p>
|
||||
|
||||
@@ -67,6 +67,7 @@ private extension SampleViewController {
|
||||
|
||||
enum RowType: Int, CaseIterable {
|
||||
case basic
|
||||
case fullScreen
|
||||
case alert
|
||||
case transientAlert
|
||||
case userGroups
|
||||
@@ -77,6 +78,7 @@ private extension SampleViewController {
|
||||
var presentable: RowPresentable {
|
||||
switch self {
|
||||
case .basic: return Basic()
|
||||
case .fullScreen: return FullScreen()
|
||||
case .alert: return Alert()
|
||||
case .transientAlert: return TransientAlert()
|
||||
case .userGroups: return UserGroup()
|
||||
@@ -86,33 +88,38 @@ private extension SampleViewController {
|
||||
}
|
||||
|
||||
struct Basic: RowPresentable {
|
||||
var string: String { return "Basic" }
|
||||
var rowVC: PanModalPresentable.LayoutType { return BasicViewController() }
|
||||
let string: String = "Basic"
|
||||
let rowVC: PanModalPresentable.LayoutType = BasicViewController()
|
||||
}
|
||||
|
||||
struct FullScreen: RowPresentable {
|
||||
let string: String = "Full Screen"
|
||||
let rowVC: PanModalPresentable.LayoutType = FullScreenNavController()
|
||||
}
|
||||
|
||||
struct Alert: RowPresentable {
|
||||
var string: String { return "Alert" }
|
||||
var rowVC: PanModalPresentable.LayoutType { return AlertViewController() }
|
||||
let string: String = "Alert"
|
||||
let rowVC: PanModalPresentable.LayoutType = AlertViewController()
|
||||
}
|
||||
|
||||
struct TransientAlert: RowPresentable {
|
||||
var string: String { return "Alert (Transient)"}
|
||||
var rowVC: PanModalPresentable.LayoutType { return TransientAlertViewController() }
|
||||
let string: String = "Alert (Transient)"
|
||||
let rowVC: PanModalPresentable.LayoutType = TransientAlertViewController()
|
||||
}
|
||||
|
||||
struct UserGroup: RowPresentable {
|
||||
var string: String { return "User Groups" }
|
||||
var rowVC: PanModalPresentable.LayoutType { return UserGroupViewController() }
|
||||
let string: String = "User Groups"
|
||||
let rowVC: PanModalPresentable.LayoutType = UserGroupViewController()
|
||||
}
|
||||
|
||||
struct Navigation: RowPresentable {
|
||||
var string: String { return "User Groups (NavigationController)" }
|
||||
var rowVC: PanModalPresentable.LayoutType { return NavigationController() }
|
||||
let string: String = "User Groups (NavigationController)"
|
||||
let rowVC: PanModalPresentable.LayoutType = NavigationController()
|
||||
}
|
||||
|
||||
struct Stacked: RowPresentable {
|
||||
var string: String { return "User Groups (Stacked)" }
|
||||
var rowVC: PanModalPresentable.LayoutType { return UserGroupStackedViewController() }
|
||||
let string: String = "User Groups (Stacked)"
|
||||
let rowVC: PanModalPresentable.LayoutType = UserGroupStackedViewController()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ class TransientAlertViewController: AlertViewController {
|
||||
return true
|
||||
}
|
||||
|
||||
override var backgroundAlpha: CGFloat {
|
||||
return 0.0
|
||||
override var panModalBackgroundColor: UIColor {
|
||||
return .clear
|
||||
}
|
||||
|
||||
override var isUserInteractionEnabled: Bool {
|
||||
|
||||
@@ -46,8 +46,8 @@ class AlertViewController: UIViewController, PanModalPresentable {
|
||||
return shortFormHeight
|
||||
}
|
||||
|
||||
var backgroundAlpha: CGFloat {
|
||||
return 0.1
|
||||
var panModalBackgroundColor: UIColor {
|
||||
return UIColor.black.withAlphaComponent(0.1)
|
||||
}
|
||||
|
||||
var shouldRoundTopCorners: Bool {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// FullScreenNavController.swift
|
||||
// PanModalDemo
|
||||
//
|
||||
// Created by Stephen Sowole on 5/2/19.
|
||||
// Copyright © 2019 Detail. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class FullScreenNavController: UINavigationController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
pushViewController(FullScreenViewController(), animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension FullScreenNavController: PanModalPresentable {
|
||||
|
||||
var panScrollable: UIScrollView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var topOffset: CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
var springDamping: CGFloat {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
var transitionDuration: Double {
|
||||
return 0.4
|
||||
}
|
||||
|
||||
var transitionAnimationOptions: UIView.AnimationOptions {
|
||||
return [.allowUserInteraction, .beginFromCurrentState]
|
||||
}
|
||||
|
||||
var shouldRoundTopCorners: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var showDragIndicator: Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private class FullScreenViewController: UIViewController {
|
||||
|
||||
let textLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "Drag downwards to dismiss"
|
||||
label.font = UIFont(name: "Lato-Bold", size: 17)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "Full Screen"
|
||||
view.backgroundColor = .white
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
view.addSubview(textLabel)
|
||||
textLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
|
||||
textLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
|
||||
}
|
||||
|
||||
}
|
||||
+9
-5
@@ -12,13 +12,17 @@ class NavigationController: UINavigationController, PanModalPresentable {
|
||||
|
||||
private let navGroups = NavUserGroups()
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
viewControllers = [navGroups]
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
pushViewController(navGroups, animated: false)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func popViewController(animated: Bool) -> UIViewController? {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class UserGroupViewController: UITableViewController, PanModalPresentable, UIGestureRecognizerDelegate {
|
||||
class UserGroupViewController: UITableViewController, PanModalPresentable {
|
||||
|
||||
let members: [UserGroupMemberPresentable] = [
|
||||
UserGroupMemberPresentable(name: "Naida Schill ✈️", role: "Staff Engineer - Mobile DevXP", avatarBackgroundColor: #colorLiteral(red: 0.7215686275, green: 0.9098039216, blue: 0.5607843137, alpha: 1)),
|
||||
|
||||
@@ -48,17 +48,21 @@ class PanModalTests: XCTestCase {
|
||||
XCTAssertEqual(vc.shortFormHeight, PanModalHeight.maxHeight)
|
||||
XCTAssertEqual(vc.longFormHeight, PanModalHeight.maxHeight)
|
||||
XCTAssertEqual(vc.springDamping, 0.8)
|
||||
XCTAssertEqual(vc.backgroundAlpha, 0.7)
|
||||
XCTAssertEqual(vc.panModalBackgroundColor, UIColor.black.withAlphaComponent(0.7))
|
||||
XCTAssertEqual(vc.dragIndicatorBackgroundColor, UIColor.lightGray)
|
||||
XCTAssertEqual(vc.scrollIndicatorInsets, .zero)
|
||||
XCTAssertEqual(vc.anchorModalToLongForm, true)
|
||||
XCTAssertEqual(vc.allowsExtendedPanScrolling, false)
|
||||
XCTAssertEqual(vc.allowsDragToDismiss, true)
|
||||
XCTAssertEqual(vc.allowsTapToDismiss, true)
|
||||
XCTAssertEqual(vc.isUserInteractionEnabled, true)
|
||||
XCTAssertEqual(vc.isHapticFeedbackEnabled, true)
|
||||
XCTAssertEqual(vc.shouldRoundTopCorners, false)
|
||||
XCTAssertEqual(vc.showDragIndicator, false)
|
||||
XCTAssertEqual(vc.shouldRoundTopCorners, false)
|
||||
XCTAssertEqual(vc.cornerRadius, 8.0)
|
||||
XCTAssertEqual(vc.transitionDuration, PanModalAnimator.Constants.defaultTransitionDuration)
|
||||
XCTAssertEqual(vc.transitionAnimationOptions, [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState])
|
||||
}
|
||||
|
||||
func testPresentableYValues() {
|
||||
|
||||
Reference in New Issue
Block a user