34 Commits

Author SHA1 Message Date
Tosin Afolabi 5d2b0977bd Swift 5.0 Support 2019-11-11 10:26:03 -08:00
Simonas Daniliauskas a792f46e3d Add customizable drag indicator background color (#62)
* Add ability to customize drag indicator background color

* Add default dragIndicatorBackgroundColor test

* update PR according to comments
2019-10-28 12:57:36 -07:00
Guillian Balisi d6b4ba3d05 Make PanModalPresentationController subclassable (#60) 2019-10-22 11:10:43 -07:00
Scott Campbell 5c1d8c49a7 Add allowTapToDismiss to PanModalPresentable (#58) 2019-10-09 13:06:51 -07:00
Stephen Sowole af264ebb0d [PanModal] Fix handleScrollViewTopBounce calls (#56)
* [PanModal] Fix calls to handleScrollViewTopBounce if scrollView is not decelerating

* [PanModal] Replace `setContentOffset` with more generic `performUpdates`

* [PanModal] Add appropriate comments
2019-10-09 13:06:12 -07:00
Santos 9d73db3919 PanModal Portrait to Landscape Issue (#53)
* Fix issue where transitioning from portrait to landscape would cause a PanModal's view to disappear if its height was short

* improve transitioning to landscape fix
2019-10-04 11:45:16 -07:00
Nikita Nikitsky 6b1edd1dfb Added the ability to select a color for the background of the pan modal container (#54) 2019-10-01 15:53:39 -07:00
Stephen Sowole 9134d20032 [PanModal] Fix unbalanced calls to appearance (#52)
* [PanModal] Use init instead of viewDidLoad in NavigationController

* [PanModal] Remove unbalanced calls to viewWillAppear/viewWillDisappear
2019-09-30 14:02:59 -07:00
Shohei Yokoyama 0c6f9ff040 Notify delegate method after the pan modal is dismissed (#44)
* Notify delegate method after the pan modal is dismissed

* Rename panModalDismissCompleted panModalDidDismiss
2019-09-22 15:25:41 -07:00
liang 12ac36646f Update README.md (#43)
Add summary and blog post link
2019-08-23 10:40:20 -07:00
Tosin Afolabi f9fedcb597 Further fix for horizontal sliding + version bump 2019-06-12 20:50:49 -07:00
Tosin Afolabi 4206308afd Version Bump to 1.2.3 2019-06-08 13:56:00 -07:00
Tosin Afolabi 0234d82979 Fix Horizonal Sliding Issues by only recogizing simultaneous gestures if the other gesture its the pan gesture (#32) 2019-06-08 13:52:59 -07:00
Giulio b0fb9e7eed Improve Swift syntax (#25)
* Replace init-ializable struct with enums

* Add AnyObject requirement to protocols

* Hide init?(coder aDecoder: NSCoder) func

* Improve Swift syntax

* Revert "Replace init-ializable struct with enums"

This reverts commit 1be2e8c23c.
2019-05-17 11:42:21 -05:00
tun57 a9edeb1d71 Update "Full Screen" mode in Sample App 2019-05-11 18:38:12 -07:00
tun57 b8f7e613ec Update PanModalTests.swift 2019-05-11 18:34:10 -07:00
tun57 8a662e829b Update PanModalPresentationAnimator.swift 2019-05-11 18:34:04 -07:00
tun57 0b2152f878 Set default transition values on PanModalPresentable 2019-05-11 18:32:07 -07:00
tun57 13251e1db7 Update PanModalAnimator.swift 2019-05-11 18:31:26 -07:00
tun57 da4c94e47e Remove redundant public accessors 2019-05-11 18:30:53 -07:00
tun57 0d999396a5 Add transitionDuration & transitionAnimationOptions to PanModalPresentatable 2019-05-11 18:29:19 -07:00
tun57 8e39823698 Default PanModalPresenter to internal 2019-05-11 18:26:20 -07:00
Stephen Sowole 457ebaed94 Add "Full Screen" mode to Sample app 2019-05-09 15:49:05 -07:00
Stephen Sowole 32e4a6fa24 No need to dismiss during size class change 2019-05-09 15:20:27 -07:00
Stephen Sowole 2736468072 Update UserGroupViewController.swift 2019-05-07 11:38:05 -07:00
Stephen Sowole c76ae09bc9 Update SampleViewController.swift 2019-05-02 16:47:02 -07:00
Stephen Sowole 12d6380d07 Add FullScreenViewController 2019-05-02 16:46:55 -07:00
Stephen Sowole 77c9f6f806 Group BasicViewController 2019-05-02 16:46:42 -07:00
Rune Madsen 964d444ff6 Adding functionality for UIViewController appearance callback forwarding (#21) 2019-04-23 22:27:37 -05:00
Stephen Sowole 9456969502 Limit when we adjust container view frame 2019-04-04 10:17:07 -07:00
Stephen Sowole 0750bd980e Update PanModalPresentationController.swift (#15) 2019-04-02 14:49:45 -07:00
Tosin Afolabi 056351af4b Update PanModal.podspec 2019-04-02 07:17:25 -07:00
Eude Lesperance dab02d34c0 Correct some docs typos (#11) 2019-03-24 20:58:33 -07:00
Stephen Sowole 96591ef52c Update PanModalPresentationController.swift 2019-03-22 14:10:06 -04:00
22 changed files with 357 additions and 122 deletions
+2 -2
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'PanModal'
s.version = '1.2'
s.version = '1.2.5'
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.
@@ -24,6 +24,6 @@ Pod::Spec.new do |s|
s.source = { :git => 'https://github.com/slackhq/PanModal.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/slackhq'
s.ios.deployment_target = '10.0'
s.swift_version = '4.2'
s.swift_version = '5.0'
s.source_files = 'PanModal/**/*.{swift,h,m}'
end
+8 -4
View File
@@ -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.
+41 -5
View File
@@ -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()
}
+1 -1
View File
@@ -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
+4 -8
View File
@@ -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)
}
+5 -2
View File
@@ -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
}
}
+29 -7
View File
@@ -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;
@@ -609,7 +629,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.slack.PanModal;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -638,7 +658,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.slack.PanModal;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -659,7 +679,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = slack.PanModalTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PanModalDemo.app/PanModalDemo";
};
@@ -679,7 +699,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = slack.PanModalTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PanModalDemo.app/PanModalDemo";
};
@@ -743,6 +763,7 @@
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@@ -797,6 +818,7 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
@@ -817,7 +839,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.PanModal;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -838,7 +860,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.PanModal;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
+10 -1
View File
@@ -1,10 +1,13 @@
### PanModal is an elegant and highly customizable presentation API for constructing bottom sheet modals on iOS.
<p align="center">
<img src="https://github.com/slackhq/PanModal/raw/master/Screenshots/panModal.gif" width="30%" height="30%" alt="Screenshot Preview" />
</p>
<p align="center">
<img src="https://img.shields.io/badge/Platform-iOS_10+-green.svg" alt="Platform: iOS 10.0+" />
<a href="https://developer.apple.com/swift" target="_blank"><img src="https://img.shields.io/badge/Language-Swift_4-blueviolet.svg" alt="Language: Swift 4" /></a>
<a href="https://developer.apple.com/swift" target="_blank"><img src="https://img.shields.io/badge/Language-Swift_5-blueviolet.svg" alt="Language: Swift 5" /></a>
<a href="https://cocoapods.org/pods/PanModal" target="_blank"><img src="https://img.shields.io/badge/CocoaPods-v1.0-red.svg" alt="CocoaPods compatible" /></a>
<a href="https://github.com/Carthage/Carthage" target="_blank"><img src="https://img.shields.io/badge/Carthage-compatible-blue.svg" alt="Carthage compatible" /></a>
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT" />
@@ -21,6 +24,12 @@
• <a href="#license">License</a>
</p>
<p align="center">
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
Swift 4.2 support can be found on the `Swift4.2` branch.
</p>
## Features
* Supports any type of `UIViewController`
+19 -12
View File
@@ -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
}
}
@@ -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)),
+5 -1
View File
@@ -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() {