Compare commits
51 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 | |||
| 1defbf2a47 | |||
| b58552279e | |||
| ef7f00dab7 | |||
| d70dce231f | |||
| 5baac6732e | |||
| 1d8b218056 | |||
| ae78bd6f64 | |||
| 2b3029333e | |||
| f71fa70302 | |||
| 6438b952cc | |||
| a7d7033ef0 | |||
| 3f3124ae37 | |||
| 0da0a44c4a | |||
| 88dc3324f6 | |||
| d0b094292f | |||
| 15f39a1929 | |||
| 21bcb6f268 | |||
| f7cb63caaa |
+1
-1
@@ -35,7 +35,7 @@ Issues labelled `good first contribution`.
|
||||
|
||||
For your contribution to be accepted:
|
||||
|
||||
- [x] You must have signed the [Contributor License Agreement (CLA)](https://cla-assistant.io/PanModal).
|
||||
- [x] You must have signed the [Contributor License Agreement (CLA)](https://cla-assistant.io/slackHQ/PanModal).
|
||||
- [x] The test suite must be complete and pass.
|
||||
- [x] The changes must be approved by code review.
|
||||
- [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number.
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'PanModal'
|
||||
s.version = '1.0.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.
|
||||
@@ -25,5 +25,5 @@ Pod::Spec.new do |s|
|
||||
s.social_media_url = 'https://twitter.com/slackhq'
|
||||
s.ios.deployment_target = '10.0'
|
||||
s.swift_version = '4.2'
|
||||
s.source_files = 'PanModal/**/*'
|
||||
s.source_files = 'PanModal/**/*.{swift,h,m}'
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -36,7 +36,6 @@ public class PanModalPresentationController: UIPresentationController {
|
||||
Constants
|
||||
*/
|
||||
struct Constants {
|
||||
static let cornerRadius = CGFloat(8.0)
|
||||
static let indicatorYOffset = CGFloat(8.0)
|
||||
static let snapMovementSensitivity = CGFloat(0.7)
|
||||
static let dragIndicatorSize = CGSize(width: 36.0, height: 5.0)
|
||||
@@ -106,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
|
||||
}()
|
||||
@@ -132,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
|
||||
}()
|
||||
@@ -186,6 +187,7 @@ public class PanModalPresentationController: UIPresentationController {
|
||||
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
self?.backgroundView.dimState = .max
|
||||
self?.presentedViewController.setNeedsStatusBarAppearanceUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -198,11 +200,12 @@ 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
|
||||
self?.backgroundView.dimState = .off
|
||||
self?.presentingViewController.setNeedsStatusBarAppearanceUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -212,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
|
||||
@@ -219,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 }
|
||||
@@ -238,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)
|
||||
@@ -341,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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,7 +409,7 @@ private extension PanModalPresentationController {
|
||||
}
|
||||
|
||||
/**
|
||||
Caluclates & stores the layout anchor points & options
|
||||
Calculates & stores the layout anchor points & options
|
||||
*/
|
||||
func configureViewLayout() {
|
||||
|
||||
@@ -413,7 +439,6 @@ private extension PanModalPresentationController {
|
||||
to avoid visual bugs
|
||||
*/
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.isScrollEnabled = presentable?.isPanScrollEnabled ?? true
|
||||
scrollView.scrollIndicatorInsets = presentable?.scrollIndicatorInsets ?? .zero
|
||||
|
||||
/**
|
||||
@@ -443,8 +468,7 @@ private extension PanModalPresentationController {
|
||||
@objc func didPanOnPresentedView(_ recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
guard
|
||||
presentable?.isPanScrollEnabled == true,
|
||||
!shouldFail(panGestureRecognizer: recognizer),
|
||||
shouldRespond(to: recognizer),
|
||||
let containerView = containerView
|
||||
else {
|
||||
recognizer.setTranslation(.zero, in: recognizer.view)
|
||||
@@ -485,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)
|
||||
|
||||
@@ -499,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)
|
||||
@@ -514,6 +538,26 @@ private extension PanModalPresentationController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Determine if the pan modal should respond to the gesture recognizer.
|
||||
|
||||
If the pan modal is already being dragged & the delegate returns false, ignore until
|
||||
the recognizer is back to it's original state (.began)
|
||||
|
||||
⚠️ This is the only time we should be cancelling the pan modal gesture recognizer
|
||||
*/
|
||||
func shouldRespond(to panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
|
||||
guard
|
||||
presentable?.shouldRespond(to: panGestureRecognizer) == true ||
|
||||
!(panGestureRecognizer.state == .began || panGestureRecognizer.state == .cancelled)
|
||||
else {
|
||||
panGestureRecognizer.isEnabled = false
|
||||
panGestureRecognizer.isEnabled = true
|
||||
return false
|
||||
}
|
||||
return !shouldFail(panGestureRecognizer: panGestureRecognizer)
|
||||
}
|
||||
|
||||
/**
|
||||
Communicate intentions to presentable and adjust subviews in containerView
|
||||
*/
|
||||
@@ -549,7 +593,7 @@ private extension PanModalPresentationController {
|
||||
Allow api consumers to override the internal conditions &
|
||||
decide if the pan gesture recognizer should be prioritized.
|
||||
|
||||
⚠️ This is the only time we should be cancelling a recognizer,
|
||||
⚠️ This is the only time we should be cancelling the panScrollable recognizer,
|
||||
for the purpose of ensuring we're no longer tracking the scrollView
|
||||
*/
|
||||
guard !shouldPrioritize(panGestureRecognizer: panGestureRecognizer) else {
|
||||
@@ -576,7 +620,7 @@ private extension PanModalPresentationController {
|
||||
*/
|
||||
func shouldPrioritize(panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
|
||||
return panGestureRecognizer.state == .began &&
|
||||
presentable?.shouldPrioritize(panModalGestureRecognizer: panGestureRecognizer) ?? false
|
||||
presentable?.shouldPrioritize(panModalGestureRecognizer: panGestureRecognizer) == true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -616,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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -633,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,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
|
||||
@@ -779,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,37 +843,23 @@ private extension PanModalPresentationController {
|
||||
because we render the dragIndicator outside of view bounds
|
||||
*/
|
||||
func addRoundedCorners(to view: UIView) {
|
||||
let radius = presentable?.cornerRadius ?? 0
|
||||
let path = UIBezierPath(roundedRect: view.bounds,
|
||||
byRoundingCorners: [.topLeft, .topRight],
|
||||
cornerRadii: CGSize(width: radius, height: radius))
|
||||
|
||||
let path = UIBezierPath()
|
||||
path.move(to: CGPoint(x: 0, y: Constants.cornerRadius))
|
||||
|
||||
// 1. Draw left rounded corner
|
||||
path.addArc(withCenter: CGPoint(x: path.currentPoint.x + Constants.cornerRadius, y: path.currentPoint.y),
|
||||
radius: Constants.cornerRadius, startAngle: .pi, endAngle: 3.0 * .pi/2.0, clockwise: true)
|
||||
|
||||
// 2. Draw around the drag indicator view, if displayed
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 3. Draw line to right side of presented view, leaving space to draw rounded corner
|
||||
path.addLine(to: CGPoint(x: view.bounds.width - Constants.cornerRadius, y: path.currentPoint.y))
|
||||
|
||||
// 4. Draw right rounded corner
|
||||
path.addArc(withCenter: CGPoint(x: path.currentPoint.x, y: path.currentPoint.y + Constants.cornerRadius),
|
||||
radius: Constants.cornerRadius, startAngle: 3.0 * .pi/2.0, endAngle: 0, clockwise: true)
|
||||
|
||||
// 5. Draw around final edges of view
|
||||
path.addLine(to: CGPoint(x: path.currentPoint.x, y: view.bounds.height))
|
||||
path.addLine(to: CGPoint(x: 0, y: path.currentPoint.y))
|
||||
|
||||
// 6. 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()
|
||||
mask.path = path.cgPath
|
||||
view.layer.mask = mask
|
||||
|
||||
// 7. Improve performance by rasterizing the layer
|
||||
// Improve performance by rasterizing the layer
|
||||
view.layer.shouldRasterize = true
|
||||
view.layer.rasterizationScale = UIScreen.main.scale
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// PanModalHeight.swift
|
||||
// SlackUI
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
@@ -35,4 +35,8 @@ public enum PanModalHeight: Equatable {
|
||||
*/
|
||||
case contentHeightIgnoringSafeArea(CGFloat)
|
||||
|
||||
/**
|
||||
Sets the height to be the intrinsic content height
|
||||
*/
|
||||
case intrinsicHeight
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// PanModalPresentable+Defaults.swift
|
||||
// PanModal
|
||||
//
|
||||
// Created by Stephen Sowole on 11/5/18.
|
||||
// Copyright © 2018 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
@@ -31,16 +30,32 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
return .contentHeight(scrollView.contentSize.height)
|
||||
}
|
||||
|
||||
var cornerRadius: CGFloat {
|
||||
return 8.0
|
||||
}
|
||||
|
||||
var springDamping: CGFloat {
|
||||
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 {
|
||||
let top = shouldRoundTopCorners ? PanModalPresentationController.Constants.cornerRadius : 0
|
||||
let top = shouldRoundTopCorners ? cornerRadius : 0
|
||||
return UIEdgeInsets(top: CGFloat(top), left: 0, bottom: bottomLayoutOffset, right: 0)
|
||||
}
|
||||
|
||||
@@ -61,7 +76,7 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
return true
|
||||
}
|
||||
|
||||
var isPanScrollEnabled: Bool {
|
||||
var allowsTapToDismiss: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -81,7 +96,11 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
return shouldRoundTopCorners
|
||||
}
|
||||
|
||||
func willRespond(to panGestureRecognizer: UIPanGestureRecognizer) {
|
||||
func shouldRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func willRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) {
|
||||
|
||||
}
|
||||
|
||||
@@ -101,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
|
||||
@@ -90,6 +90,12 @@ extension PanModalPresentable where Self: UIViewController {
|
||||
return bottomYPos - (height + bottomLayoutOffset)
|
||||
case .contentHeightIgnoringSafeArea(let height):
|
||||
return bottomYPos - height
|
||||
case .intrinsicHeight:
|
||||
view.layoutIfNeeded()
|
||||
let targetSize = CGSize(width: (presentedVC?.containerView?.bounds ?? UIScreen.main.bounds).width,
|
||||
height: UIView.layoutFittingCompressedSize.height)
|
||||
let intrinsicHeight = view.systemLayoutSizeFitting(targetSize).height
|
||||
return bottomYPos - (intrinsicHeight + bottomLayoutOffset)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ import UIKit
|
||||
|
||||
Usage:
|
||||
```
|
||||
extension UIViewController: PanModalPresentable {
|
||||
extension YourViewController: PanModalPresentable {
|
||||
func shouldRoundTopCorners: Bool { return false }
|
||||
}
|
||||
```
|
||||
*/
|
||||
public protocol PanModalPresentable {
|
||||
public protocol PanModalPresentable: AnyObject {
|
||||
|
||||
/**
|
||||
The scroll view embedded in the view controller.
|
||||
@@ -54,6 +54,13 @@ public protocol PanModalPresentable {
|
||||
*/
|
||||
var longFormHeight: PanModalHeight { get }
|
||||
|
||||
/**
|
||||
The corner radius used when `shouldRoundTopCorners` is enabled.
|
||||
|
||||
Default Value is 8.0.
|
||||
*/
|
||||
var cornerRadius: CGFloat { get }
|
||||
|
||||
/**
|
||||
The springDamping value used to determine the amount of 'bounce'
|
||||
seen when transitioning to short/long form.
|
||||
@@ -63,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
|
||||
@@ -91,13 +121,13 @@ public protocol PanModalPresentable {
|
||||
A flag to determine if scrolling should seamlessly transition from the pan modal container view to
|
||||
the embedded scroll view once the scroll limit has been reached.
|
||||
|
||||
Default value is false.
|
||||
Unless a scrollView is provided and the content exceeds the longForm height
|
||||
Default value is false. Unless a scrollView is provided and the content height exceeds the longForm height.
|
||||
*/
|
||||
var allowsExtendedPanScrolling: Bool { get }
|
||||
|
||||
/**
|
||||
A flag to determine if dismissal should be initiated when swiping down on the presented view.
|
||||
|
||||
Return false to fallback to the short form state instead of dismissing.
|
||||
|
||||
Default value is true.
|
||||
@@ -105,14 +135,11 @@ public protocol PanModalPresentable {
|
||||
var allowsDragToDismiss: Bool { get }
|
||||
|
||||
/**
|
||||
A flag to determine if scrolling should be enabled on the entire view.
|
||||
|
||||
- Note: Returning false will disable scrolling on the embedded scrollview as well as on the
|
||||
pan modal container view.
|
||||
A flag to determine if dismissal should be initiated when tapping on the dimmed background view.
|
||||
|
||||
Default value is true.
|
||||
*/
|
||||
var isPanScrollEnabled: Bool { get }
|
||||
var allowsTapToDismiss: Bool { get }
|
||||
|
||||
/**
|
||||
A flag to toggle user interactions on the container view.
|
||||
@@ -145,6 +172,15 @@ public protocol PanModalPresentable {
|
||||
*/
|
||||
var showDragIndicator: Bool { get }
|
||||
|
||||
/**
|
||||
Asks the delegate if the pan modal should respond to the pan modal gesture recognizer.
|
||||
|
||||
Return false to disable movement on the pan modal but maintain gestures on the presented view.
|
||||
|
||||
Default value is true.
|
||||
*/
|
||||
func shouldRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool
|
||||
|
||||
/**
|
||||
Notifies the delegate when the pan modal gesture recognizer state is either
|
||||
`began` or `changed`. This method gives the delegate a chance to prepare
|
||||
@@ -154,7 +190,7 @@ public protocol PanModalPresentable {
|
||||
|
||||
Default value is an empty implementation.
|
||||
*/
|
||||
func willRespond(to panGestureRecognizer: UIPanGestureRecognizer)
|
||||
func willRespond(to panModalGestureRecognizer: UIPanGestureRecognizer)
|
||||
|
||||
/**
|
||||
Asks the delegate if the pan modal gesture recognizer should be prioritized.
|
||||
@@ -162,8 +198,8 @@ public protocol PanModalPresentable {
|
||||
For example, you can use this to define a region
|
||||
where you would like to restrict where the pan gesture can start.
|
||||
|
||||
If false, then we rely on the internal conditions of when a pan gesture
|
||||
should succeed or fail, such as, if we're actively scrolling on the scrollView
|
||||
If false, then we rely solely on the internal conditions of when a pan gesture
|
||||
should succeed or fail, such as, if we're actively scrolling on the scrollView.
|
||||
|
||||
Default return value is false.
|
||||
*/
|
||||
@@ -190,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()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// PanModalPresenter.swift
|
||||
// SlackUI
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// UIViewController+PanModalPresenterProtocol.swift
|
||||
// SlackUI
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
@@ -50,6 +50,7 @@ extension UIViewController: PanModalPresenter {
|
||||
viewControllerToPresent.popoverPresentationController?.delegate = PanModalPresentationDelegate.default
|
||||
} else {
|
||||
viewControllerToPresent.modalPresentationStyle = .custom
|
||||
viewControllerToPresent.modalPresentationCapturesStatusBarAppearance = true
|
||||
viewControllerToPresent.transitioningDelegate = PanModalPresentationDelegate.default
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -661,7 +681,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PanModal.app/PanModal";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PanModalDemo.app/PanModalDemo";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -681,7 +701,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PanModal.app/PanModal";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PanModalDemo.app/PanModalDemo";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@@ -28,6 +28,16 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "743CABC32226171500634A5A"
|
||||
BuildableName = "PanModalTests.xctest"
|
||||
BlueprintName = "PanModalTests"
|
||||
ReferencedContainer = "container:PanModalDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+4
@@ -18,6 +18,10 @@ class BasicViewController: UIViewController {
|
||||
|
||||
extension BasicViewController: PanModalPresentable {
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
var panScrollable: UIScrollView? {
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
+11
-3
@@ -12,9 +12,17 @@ class NavigationController: UINavigationController, PanModalPresentable {
|
||||
|
||||
private let navGroups = NavUserGroups()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
pushViewController(navGroups, animated: false)
|
||||
init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
viewControllers = [navGroups]
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func popViewController(animated: Bool) -> UIViewController? {
|
||||
|
||||
@@ -14,6 +14,10 @@ class StackedProfileViewController: UIViewController, PanModalPresentable {
|
||||
|
||||
let presentable: UserGroupMemberPresentable
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
let avatarView: UIView = {
|
||||
@@ -85,6 +89,7 @@ class StackedProfileViewController: UIViewController, PanModalPresentable {
|
||||
|
||||
roleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
|
||||
roleLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4.0).isActive = true
|
||||
bottomLayoutGuide.topAnchor.constraint(greaterThanOrEqualTo: roleLabel.bottomAnchor).isActive = true
|
||||
}
|
||||
|
||||
// MARK: - Pan Modal Presentable
|
||||
@@ -94,7 +99,7 @@ class StackedProfileViewController: UIViewController, PanModalPresentable {
|
||||
}
|
||||
|
||||
var longFormHeight: PanModalHeight {
|
||||
return .contentHeight(300)
|
||||
return .intrinsicHeight
|
||||
}
|
||||
|
||||
var anchorModalToLongForm: Bool {
|
||||
|
||||
@@ -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)),
|
||||
@@ -34,6 +34,10 @@ class UserGroupViewController: UITableViewController, PanModalPresentable, UIGes
|
||||
|
||||
var isShortFormEnabled = true
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
let headerView = UserGroupHeaderView()
|
||||
|
||||
let headerPresentable = UserGroupHeaderPresentable.init(handle: "ios-engs", description: "iOS Engineers", memberCount: 10)
|
||||
|
||||
@@ -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.isPanScrollEnabled, 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