Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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'
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
@@ -186,6 +185,7 @@ public class PanModalPresentationController: UIPresentationController {
|
||||
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
self?.backgroundView.dimState = .max
|
||||
self?.presentedViewController.setNeedsStatusBarAppearanceUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -203,6 +203,7 @@ public class PanModalPresentationController: UIPresentationController {
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
self?.dragIndicatorView.alpha = 0.0
|
||||
self?.backgroundView.dimState = .off
|
||||
self?.presentingViewController.setNeedsStatusBarAppearanceUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -413,7 +414,6 @@ private extension PanModalPresentationController {
|
||||
to avoid visual bugs
|
||||
*/
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.isScrollEnabled = presentable?.isPanScrollEnabled ?? true
|
||||
scrollView.scrollIndicatorInsets = presentable?.scrollIndicatorInsets ?? .zero
|
||||
|
||||
/**
|
||||
@@ -443,8 +443,7 @@ private extension PanModalPresentationController {
|
||||
@objc func didPanOnPresentedView(_ recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
guard
|
||||
presentable?.isPanScrollEnabled == true,
|
||||
!shouldFail(panGestureRecognizer: recognizer),
|
||||
shouldRespond(to: panGestureRecognizer),
|
||||
let containerView = containerView
|
||||
else {
|
||||
recognizer.setTranslation(.zero, in: recognizer.view)
|
||||
@@ -514,6 +513,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 +568,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 +595,7 @@ private extension PanModalPresentationController {
|
||||
*/
|
||||
func shouldPrioritize(panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
|
||||
return panGestureRecognizer.state == .began &&
|
||||
presentable?.shouldPrioritize(panModalGestureRecognizer: panGestureRecognizer) ?? false
|
||||
presentable?.shouldPrioritize(panModalGestureRecognizer: panGestureRecognizer) == true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -797,37 +816,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
|
||||
}
|
||||
|
||||
@@ -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,6 +30,10 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
return .contentHeight(scrollView.contentSize.height)
|
||||
}
|
||||
|
||||
var cornerRadius: CGFloat {
|
||||
return 8.0
|
||||
}
|
||||
|
||||
var springDamping: CGFloat {
|
||||
return 0.8
|
||||
}
|
||||
@@ -40,7 +43,7 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
}
|
||||
|
||||
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,10 +64,6 @@ public extension PanModalPresentable where Self: UIViewController {
|
||||
return true
|
||||
}
|
||||
|
||||
var isPanScrollEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var isUserInteractionEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
@@ -81,7 +80,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) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import UIKit
|
||||
|
||||
Usage:
|
||||
```
|
||||
extension UIViewController: PanModalPresentable {
|
||||
extension YourViewController: PanModalPresentable {
|
||||
func shouldRoundTopCorners: Bool { return false }
|
||||
}
|
||||
```
|
||||
@@ -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.
|
||||
@@ -91,29 +98,19 @@ 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.
|
||||
*/
|
||||
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.
|
||||
|
||||
Default value is true.
|
||||
*/
|
||||
var isPanScrollEnabled: Bool { get }
|
||||
|
||||
/**
|
||||
A flag to toggle user interactions on the container view.
|
||||
|
||||
@@ -145,6 +142,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 +160,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 +168,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.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// PanModalPresenter.swift
|
||||
// SlackUI
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -661,7 +661,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 +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 = 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
|
||||
|
||||
@@ -18,6 +18,10 @@ class BasicViewController: UIViewController {
|
||||
|
||||
extension BasicViewController: PanModalPresentable {
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
var panScrollable: UIScrollView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ class NavigationController: UINavigationController, PanModalPresentable {
|
||||
|
||||
private let navGroups = NavUserGroups()
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
pushViewController(navGroups, animated: false)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -53,12 +53,12 @@ class PanModalTests: XCTestCase {
|
||||
XCTAssertEqual(vc.anchorModalToLongForm, true)
|
||||
XCTAssertEqual(vc.allowsExtendedPanScrolling, false)
|
||||
XCTAssertEqual(vc.allowsDragToDismiss, true)
|
||||
XCTAssertEqual(vc.isPanScrollEnabled, 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)
|
||||
}
|
||||
|
||||
func testPresentableYValues() {
|
||||
|
||||
Reference in New Issue
Block a user