Compare commits

...

34 Commits

Author SHA1 Message Date
Ivan Smolin ced7c1703f Merge pull request 'make DimmedView init public' (#5) from feature/dimmed_view_public_init into master
Reviewed-on: #5
2023-07-26 13:59:02 +03:00
Ivan Smolin 36be7f0f25 make DimmedView init public 2023-07-26 13:58:13 +03:00
Ivan Smolin b8f07ab26b Merge pull request 'update source url of podspec' (#4) from update_podspec_source_url into master
Reviewed-on: #4
2023-07-25 16:54:41 +03:00
Ivan Smolin 1c47459734 update source url of podspec 2023-07-25 16:53:39 +03:00
Ivan Smolin be82eddb52 Merge pull request 'fix merge issues' (#3) from fix_merge into master
Reviewed-on: TouchInstinct/TIPanModal#3
2023-07-25 16:19:01 +03:00
Ivan Smolin 73c00b4563 fix merge issues 2023-07-25 16:17:51 +03:00
Ivan Smolin 8ac7096ec9 Merge pull request 'feature/minimize_pan_modal_changes' (#2) from feature/minimize_pan_modal_changes into master
Reviewed-on: TouchInstinct/TIPanModal#2
2023-07-25 15:40:33 +03:00
Ivan Smolin 0c3ed5a2ef update source urls 2023-07-25 15:38:31 +03:00
Ivan Smolin eaf349654d Merge branch 'master' into feature/minimize_pan_modal_changes 2023-07-25 15:36:33 +03:00
Ivan Smolin 83d4b7024c Add mediumForm and open DimmedView customization 2023-07-25 14:30:56 +03:00
Nikita Semenov 871fc46020 Merge pull request 'feat: detents api' (#1) from feature/detents_api into master
Reviewed-on: TouchInstinct/TIPanModal#1
2023-07-09 10:58:44 +03:00
Nikita Semenov d28aab13a6 feat: detents api 2023-07-09 10:57:15 +03:00
Jordan Pichler b2f5bd7d16
Add completion block to present function (#94) 2020-04-21 11:16:30 -07:00
Tosin Afolabi b012aecb6b bump version number. 2020-03-30 12:33:24 -07:00
Stephen Sowole 22b4ddd47e
[PanModal] Track all dismiss events through panModalPresentable callbacks (#91) 2020-03-25 00:34:58 -07:00
Stephen Sowole 17a8231f20 Update README.md 2020-03-24 17:10:08 -07:00
Stephen Sowole 1819113e97 [ReadMe] Add link to Swift Package Manager 2020-02-27 09:10:21 -08:00
Stephen Sowole 208640e03e
[PanModal] Round frame positions for comparison (#83) 2020-02-24 15:20:26 -08:00
Stephen Sowole f02439dc0c
[PanModal] Fix issue with incorrect returned bottom offset value (#84) 2020-02-24 15:20:14 -08:00
Kyohei Ito 047415090a
Fix Background dimming animation broken (#77) 2020-02-24 14:02:45 -08:00
Abdullah Selek 1e7c0534fc
Use keyPath to read UIApplication.shared. (#81) 2020-02-12 13:02:33 -06:00
Tosin Afolabi 7f07cdff27 Initial SPM Support 2019-11-11 10:52:18 -08:00
Nikita Nikitsky 45f8dfcf19 Add support for Swift Package Manager (#55) 2019-11-11 10:49:59 -08:00
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
23 changed files with 388 additions and 131 deletions

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

22
Package.swift Normal file
View File

@ -0,0 +1,22 @@
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "PanModal",
platforms: [.iOS(.v10)],
products: [
.library(
name: "PanModal",
targets: ["PanModal"]),
],
dependencies: [],
targets: [
.target(
name: "PanModal",
dependencies: [],
path: "PanModal")
],
swiftLanguageVersions: [.version("5.0")]
)

View File

@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'PanModal'
s.version = '1.2.3'
s.version = '1.3.1'
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.
@ -18,12 +18,12 @@ Pod::Spec.new do |s|
# * Finally, don't worry about the indent, CocoaPods strips it!
s.description = 'PanModal is an elegant and highly customizable presentation API for constructing bottom sheet modals on iOS.'
s.homepage = 'https://github.com/slackhq/PanModal'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/PanModal'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'slack' => 'opensource@slack.com' }
s.source = { :git => 'https://github.com/slackhq/PanModal.git', :tag => s.version.to_s }
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/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

View File

@ -5,6 +5,7 @@
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -36,3 +37,4 @@ struct PanModalAnimator {
completion: completion)
}
}
#endif

View File

@ -5,6 +5,7 @@
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -72,7 +73,6 @@ public class PanModalPresentationAnimator: NSObject {
// Calls viewWillAppear and viewWillDisappear
fromVC.beginAppearanceTransition(false, animated: true)
toVC.beginAppearanceTransition(true, animated: true)
// Presents the view in shortForm position, initially
let yPos: CGFloat = presentable?.shortFormYPos ?? 0.0
@ -94,7 +94,6 @@ public class PanModalPresentationAnimator: NSObject {
}, config: presentable) { [weak self] didComplete in
// Calls viewDidAppear and viewDidDisappear
fromVC.endAppearanceTransition()
toVC.endAppearanceTransition()
transitionContext.completeTransition(didComplete)
self?.feedbackGenerator = nil
}
@ -111,7 +110,6 @@ public class PanModalPresentationAnimator: NSObject {
else { return }
// Calls viewWillAppear and viewWillDisappear
fromVC.beginAppearanceTransition(false, animated: true)
toVC.beginAppearanceTransition(true, animated: true)
let presentable = panModalLayoutType(from: transitionContext)
@ -122,7 +120,6 @@ public class PanModalPresentationAnimator: NSObject {
}, config: presentable) { didComplete in
fromVC.view.removeFromSuperview()
// Calls viewDidAppear and viewDidDisappear
fromVC.endAppearanceTransition()
toVC.endAppearanceTransition()
transitionContext.completeTransition(didComplete)
}
@ -172,3 +169,4 @@ extension PanModalPresentationAnimator: UIViewControllerAnimatedTransitioning {
}
}
#endif

View File

@ -5,6 +5,7 @@
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -22,13 +23,14 @@ 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
*/
public enum PresentationState {
case shortForm
case mediumForm
case longForm
}
@ -78,11 +80,17 @@ public class PanModalPresentationController: UIPresentationController {
*/
private var shortFormYPosition: CGFloat = 0
private var mediumFormYPosition: CGFloat = 0
/**
The y value for the long form presentation state
*/
private var longFormYPosition: CGFloat = 0
private var allowsDragToDismiss: Bool {
presentable?.onDragToDismiss != nil
}
/**
Determine anchored Y postion based on the `anchorModalToLongForm` flag
*/
@ -104,14 +112,14 @@ public class PanModalPresentationController: UIPresentationController {
Background view used as an overlay over the presenting view
*/
private lazy var backgroundView: DimmedView = {
let view: DimmedView
if let alpha = presentable?.backgroundAlpha {
view = DimmedView(dimAlpha: alpha)
} else {
view = DimmedView()
let view: DimmedView = presentable?.dimmedView ?? DimmedView()
if let color = presentable?.panModalBackgroundColor {
view.backgroundColor = color
}
view.didTap = { [weak self] _ in
self?.dismissPresentedViewController()
if self?.presentable?.allowsTapToDismiss == true {
self?.presentable?.onTapToDismiss?()
}
}
return view
}()
@ -131,7 +139,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
}()
@ -184,12 +192,21 @@ public class PanModalPresentationController: UIPresentationController {
}
coordinator.animate(alongsideTransition: { [weak self] _ in
self?.backgroundView.dimState = .max
if let yPos = self?.shortFormYPosition {
self?.adjust(toYPosition: yPos)
}
self?.presentedViewController.setNeedsStatusBarAppearanceUpdate()
})
}
override public func presentationTransitionDidEnd(_ completed: Bool) {
if completed { return }
backgroundView.removeFromSuperview()
}
override public func dismissalTransitionWillBegin() {
presentable?.panModalWillDismiss()
guard let coordinator = presentedViewController.transitionCoordinator else {
backgroundView.dimState = .off
@ -207,10 +224,10 @@ public class PanModalPresentationController: UIPresentationController {
})
}
override public func presentationTransitionDidEnd(_ completed: Bool) {
if completed { return }
backgroundView.removeFromSuperview()
override public func dismissalTransitionDidEnd(_ completed: Bool) {
if !completed { return }
presentable?.panModalDidDismiss()
}
/**
@ -226,7 +243,6 @@ public class PanModalPresentationController: UIPresentationController {
else { return }
self.adjustPresentedViewFrame()
if presentable.shouldRoundTopCorners {
self.addRoundedCorners(to: self.presentedView)
}
@ -253,41 +269,37 @@ public extension PanModalPresentationController {
switch state {
case .shortForm:
snap(toYPosition: shortFormYPosition)
case .mediumForm:
snap(toYPosition: mediumFormYPosition)
case .longForm:
snap(toYPosition: longFormYPosition)
}
}
/**
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 change
and then resumes content offset observation.
To avoid this, you can call this method to perform scroll view updates,
with scroll observation temporarily disabled.
*/
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)
}
@ -317,7 +329,7 @@ private extension PanModalPresentationController {
var isPresentedViewAnchored: Bool {
if !isPresentedViewAnimating
&& extendsPanScrolling
&& presentedView.frame.minY <= anchoredYPosition {
&& presentedView.frame.minY.rounded() <= anchoredYPosition.rounded() {
return true
}
@ -367,7 +379,18 @@ private extension PanModalPresentationController {
else { return }
let adjustedSize = CGSize(width: frame.size.width, height: frame.size.height - anchoredYPosition)
let panFrame = panContainerView.frame
panContainerView.frame.size = frame.size
let positions = [shortFormYPosition, mediumFormYPosition, longFormYPosition]
if !positions.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)
let yPosition = panFrame.origin.y - panFrame.height + frame.height
presentedView.frame.origin.y = max(yPosition, anchoredYPosition)
}
panContainerView.frame.origin.x = frame.origin.x
presentedViewController.view.frame = CGRect(origin: .zero, size: adjustedSize)
}
@ -416,6 +439,7 @@ private extension PanModalPresentationController {
else { return }
shortFormYPosition = layoutPresentable.shortFormYPos
mediumFormYPosition = layoutPresentable.mediumFormYPos
longFormYPosition = layoutPresentable.longFormYPos
anchorModalToLongForm = layoutPresentable.anchorModalToLongForm
extendsPanScrolling = layoutPresentable.allowsExtendedPanScrolling
@ -444,7 +468,15 @@ private extension PanModalPresentationController {
Set the appropriate contentInset as the configuration within this class
offsets it
*/
scrollView.contentInset.bottom = presentingViewController.bottomLayoutGuide.length
let bottomInset: CGFloat
if #available(iOS 11.0, *) {
bottomInset = presentingViewController.view.safeAreaInsets.bottom
} else {
bottomInset = presentingViewController.bottomLayoutGuide.length
}
scrollView.contentInset.bottom = bottomInset
/**
As we adjust the bounds during `handleScrollViewTopBounce`
@ -508,12 +540,18 @@ private extension PanModalPresentationController {
if velocity.y < 0 {
transition(to: .longForm)
} else if (nearest(to: presentedView.frame.minY, inValues: [longFormYPosition, containerView.bounds.height]) == longFormYPosition
&& presentedView.frame.minY < shortFormYPosition) || presentable?.allowsDragToDismiss == false {
} else if nearest(to: presentedView.frame.minY,
inValues: [mediumFormYPosition, containerView.bounds.height]) == mediumFormYPosition
&& presentedView.frame.minY < mediumFormYPosition {
transition(to: .mediumForm)
} else if (nearest(to: presentedView.frame.minY,
inValues: [longFormYPosition, containerView.bounds.height]) == longFormYPosition
&& presentedView.frame.minY < shortFormYPosition) || allowsDragToDismiss == false {
transition(to: .shortForm)
} else {
dismissPresentedViewController()
presentable?.onDragToDismiss?()
}
} else {
@ -522,16 +560,23 @@ 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 = nearest(to: presentedView.frame.minY, inValues: [containerView.bounds.height, shortFormYPosition, longFormYPosition])
let position = nearest(to: presentedView.frame.minY,
inValues: [containerView.bounds.height,
shortFormYPosition,
mediumFormYPosition,
longFormYPosition])
if position == longFormYPosition {
transition(to: .longForm)
} else if position == mediumFormYPosition {
transition(to: .mediumForm)
} else if position == shortFormYPosition || presentable?.allowsDragToDismiss == false {
} else if position == shortFormYPosition || allowsDragToDismiss == false {
transition(to: .shortForm)
} else {
dismissPresentedViewController()
presentable?.onDragToDismiss?()
}
}
}
@ -643,19 +688,10 @@ private extension PanModalPresentationController {
*/
func adjust(toYPosition yPos: CGFloat) {
presentedView.frame.origin.y = max(yPos, anchoredYPosition)
guard presentedView.frame.origin.y > shortFormYPosition else {
backgroundView.dimState = .max
return
}
let yDisplacementFromShortForm = presentedView.frame.origin.y - shortFormYPosition
let maxHeight = UIScreen.main.bounds.height - longFormYPosition
/**
Once presentedView is translated below shortForm, calculate yPos relative to bottom of screen
and apply percentage to backgroundView alpha
*/
backgroundView.dimState = .percent(1.0 - (yDisplacementFromShortForm / presentedView.frame.height))
backgroundView.dimState = .percent(1.0 - presentedView.frame.origin.y / maxHeight)
}
/**
@ -670,14 +706,6 @@ private extension PanModalPresentationController {
else { return number }
return nearestVal
}
/**
Dismiss presented view
*/
func dismissPresentedViewController() {
presentable?.panModalWillDismiss()
presentedViewController.dismiss(animated: true, completion: nil)
}
}
// MARK: - UIScrollView Observer
@ -782,7 +810,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
@ -822,11 +850,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 == panGestureRecognizer
return otherGestureRecognizer.view == presentable?.panScrollable
}
}
@ -887,3 +915,4 @@ private extension UIScrollView {
return isDragging && !isDecelerating || isTracking
}
}
#endif

View File

@ -5,6 +5,7 @@
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -77,3 +78,4 @@ extension PanModalPresentationDelegate: UIAdaptivePresentationControllerDelegate
}
}
#endif

View File

@ -0,0 +1,66 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
public struct ModalViewPresentationDetent: Hashable {
// MARK: - Default Values
public static var headerOnly: ModalViewPresentationDetent {
ModalViewPresentationDetent(height: CGFloat(Int.min))
}
public static func height(_ height: CGFloat) -> ModalViewPresentationDetent {
ModalViewPresentationDetent(height: height)
}
public static var maxHeight: ModalViewPresentationDetent {
ModalViewPresentationDetent(height: CGFloat(Int.max))
}
// MARK: - Public Properties
public var height: CGFloat
// MARK: - Internal Methods
func panModalHeight(headerHeight: CGFloat = .zero) -> PanModalHeight {
if self == .headerOnly {
return .contentHeight(headerHeight)
}
if self == .maxHeight {
return .maxHeight
}
return .contentHeight(height)
}
}
// MARK: - Comparable
extension ModalViewPresentationDetent: Comparable {
public static func < (lhs: ModalViewPresentationDetent, rhs: ModalViewPresentationDetent) -> Bool {
lhs.height < rhs.height
}
}

View File

@ -5,6 +5,7 @@
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -40,3 +41,4 @@ public enum PanModalHeight: Equatable {
*/
case intrinsicHeight
}
#endif

View File

@ -5,6 +5,7 @@
// Copyright © 2018 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -12,14 +13,30 @@ import UIKit
*/
public extension PanModalPresentable where Self: UIViewController {
var onTapToDismiss: (() -> Void)? {
{ [weak self] in
self?.dismiss(animated: true)
}
}
var onDragToDismiss: (() -> Void)? {
{ [weak self] in
self?.dismiss(animated: true)
}
}
var topOffset: CGFloat {
return topLayoutOffset + 21.0
topLayoutOffset
}
var shortFormHeight: PanModalHeight {
return longFormHeight
}
var mediumFormHeight: PanModalHeight {
longFormHeight
}
var longFormHeight: PanModalHeight {
guard let scrollView = panScrollable
@ -30,6 +47,10 @@ public extension PanModalPresentable where Self: UIViewController {
return .contentHeight(scrollView.contentSize.height)
}
var dimmedView: DimmedView? {
DimmedView()
}
var cornerRadius: CGFloat {
return 8.0
}
@ -46,8 +67,12 @@ public extension PanModalPresentable where Self: UIViewController {
return [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState]
}
var backgroundAlpha: CGFloat {
return 0.7
var panModalBackgroundColor: UIColor {
return UIColor.black.withAlphaComponent(0.7)
}
var dragIndicatorBackgroundColor: UIColor {
return UIColor.lightGray
}
var scrollIndicatorInsets: UIEdgeInsets {
@ -72,6 +97,10 @@ public extension PanModalPresentable where Self: UIViewController {
return true
}
var allowsTapToDismiss: Bool {
return true
}
var isUserInteractionEnabled: Bool {
return true
}
@ -112,4 +141,8 @@ public extension PanModalPresentable where Self: UIViewController {
}
func panModalDidDismiss() {
}
}
#endif

View File

@ -5,6 +5,7 @@
// Copyright © 2018 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -26,7 +27,11 @@ extension PanModalPresentable where Self: UIViewController {
Gives us the safe area inset from the top.
*/
var topLayoutOffset: CGFloat {
return UIApplication.shared.keyWindow?.rootViewController?.topLayoutGuide.length ?? 0
guard let rootVC = rootViewController
else { return 0}
if #available(iOS 11.0, *) { return rootVC.view.safeAreaInsets.top } else { return rootVC.topLayoutGuide.length }
}
/**
@ -34,7 +39,11 @@ extension PanModalPresentable where Self: UIViewController {
Gives us the safe area inset from the bottom.
*/
var bottomLayoutOffset: CGFloat {
return UIApplication.shared.keyWindow?.rootViewController?.bottomLayoutGuide.length ?? 0
guard let rootVC = rootViewController
else { return 0}
if #available(iOS 11.0, *) { return rootVC.view.safeAreaInsets.bottom } else { return rootVC.bottomLayoutGuide.length }
}
/**
@ -54,6 +63,12 @@ extension PanModalPresentable where Self: UIViewController {
return max(shortFormYPos, longFormYPos)
}
var mediumFormYPos: CGFloat {
let mediumFormYPos = topMargin(from: mediumFormHeight)
return max(mediumFormYPos, longFormYPos)
}
/**
Returns the long form Y position
@ -99,4 +114,13 @@ extension PanModalPresentable where Self: UIViewController {
}
}
private var rootViewController: UIViewController? {
guard let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication
else { return nil }
return application.keyWindow?.rootViewController
}
}
#endif

View File

@ -5,6 +5,7 @@
// Copyright © 2018 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -29,16 +30,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 +40,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.
@ -59,3 +60,4 @@ public extension PanModalPresentable where Self: UIViewController {
}
}
#endif

View File

@ -5,6 +5,7 @@
// Copyright © 2017 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -44,6 +45,8 @@ public protocol PanModalPresentable: AnyObject {
*/
var shortFormHeight: PanModalHeight { get }
var mediumFormHeight: PanModalHeight { get }
/**
The height of the pan modal container view
when in the longForm presentation state.
@ -54,6 +57,7 @@ public protocol PanModalPresentable: AnyObject {
*/
var longFormHeight: PanModalHeight { get }
var dimmedView: DimmedView? { get }
/**
The corner radius used when `shouldRoundTopCorners` is enabled.
@ -86,13 +90,20 @@ public protocol PanModalPresentable: AnyObject {
var transitionAnimationOptions: UIView.AnimationOptions { get }
/**
The background view alpha.
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
@ -127,6 +138,17 @@ public protocol PanModalPresentable: AnyObject {
*/
var allowsDragToDismiss: Bool { get }
var onTapToDismiss: (() -> Void)? { get }
var onDragToDismiss: (() -> Void)? { 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.
@ -212,4 +234,11 @@ public protocol PanModalPresentable: AnyObject {
*/
func panModalWillDismiss()
/**
Notifies the delegate after the pan modal is dismissed.
Default value is an empty implementation.
*/
func panModalDidDismiss()
}
#endif

View File

@ -5,6 +5,7 @@
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -28,6 +29,10 @@ protocol PanModalPresenter: AnyObject {
/**
Presents a view controller that conforms to the PanModalPresentable protocol
*/
func presentPanModal(_ viewControllerToPresent: PanModalPresentable.LayoutType, sourceView: UIView?, sourceRect: CGRect)
func presentPanModal(_ viewControllerToPresent: PanModalPresentable.LayoutType,
sourceView: UIView?,
sourceRect: CGRect,
completion: (() -> Void)?)
}
#endif

View File

@ -5,6 +5,7 @@
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -34,10 +35,14 @@ extension UIViewController: PanModalPresenter {
- viewControllerToPresent: The view controller to be presented
- sourceView: The view containing the anchor rectangle for the popover.
- sourceRect: The rectangle in the specified view in which to anchor the popover.
- completion: The block to execute after the presentation finishes. You may specify nil for this parameter.
- Note: sourceView & sourceRect are only required for presentation on an iPad.
*/
public func presentPanModal(_ viewControllerToPresent: PanModalPresentable.LayoutType, sourceView: UIView? = nil, sourceRect: CGRect = .zero) {
public func presentPanModal(_ viewControllerToPresent: PanModalPresentable.LayoutType,
sourceView: UIView? = nil,
sourceRect: CGRect = .zero,
completion: (() -> Void)? = nil) {
/**
Here, we deliberately do not check for size classes. More info in `PanModalPresentationDelegate`
@ -54,7 +59,8 @@ extension UIViewController: PanModalPresenter {
viewControllerToPresent.transitioningDelegate = PanModalPresentationDelegate.default
}
present(viewControllerToPresent, animated: true, completion: nil)
present(viewControllerToPresent, animated: true, completion: completion)
}
}
#endif

View File

@ -5,18 +5,19 @@
// Copyright © 2017 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
A dim view for use as an overlay over content you want dimmed.
*/
public class DimmedView: UIView {
open class DimmedView: UIView {
/**
Represents the possible states of the dimmed view.
max, off or a percentage of dimAlpha.
*/
enum DimState {
public enum DimState {
case max
case off
case percent(CGFloat)
@ -29,22 +30,14 @@ public class DimmedView: UIView {
*/
var dimState: DimState = .off {
didSet {
switch dimState {
case .max:
alpha = dimAlpha
case .off:
alpha = 0.0
case .percent(let percentage):
let val = max(0.0, min(1.0, percentage))
alpha = dimAlpha * val
}
onChange(dimState: dimState)
}
}
/**
The closure to be executed when a tap occurs
*/
var didTap: ((_ recognizer: UIGestureRecognizer) -> Void)?
var didTap: ((_ recognizer: UITapGestureRecognizer) -> Void)?
/**
Tap gesture recognizer
@ -53,15 +46,11 @@ 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
public init() {
super.init(frame: .zero)
alpha = 0.0
backgroundColor = .black
addGestureRecognizer(tapGesture)
}
@ -71,8 +60,22 @@ public class DimmedView: UIView {
// MARK: - Event Handlers
@objc private func didTapView() {
didTap?(tapGesture)
@objc private func didTapView(sender: UITapGestureRecognizer) {
didTap?(sender)
}
// MARK: - Subclass override
open func onChange(dimState: DimState) {
switch dimState {
case .max:
alpha = 1.0
case .off:
alpha = 0.0
case .percent(let percentage):
alpha = max(0.0, min(1.0, percentage))
}
}
}
#endif

View File

@ -5,6 +5,7 @@
// Copyright © 2018 Tiny Speck, Inc. All rights reserved.
//
#if os(iOS)
import UIKit
/**
@ -40,3 +41,4 @@ extension UIView {
}
}
#endif

View File

@ -629,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 = "";
@ -658,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 = "";
@ -679,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";
};
@ -699,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";
};
@ -763,6 +763,7 @@
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@ -817,6 +818,7 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
@ -837,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;
@ -858,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;

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`
@ -45,6 +54,14 @@ pod 'PanModal'
github "slackhq/PanModal"
```
* <a href="https://swift.org/package-manager/" target="_blank">Swift Package Manager</a>:
```swift
dependencies: [
.package(url: "https://github.com/slackhq/PanModal.git", .exact("1.2.6")),
],
```
## Usage
PanModal was designed to be used effortlessly. Simply call `presentPanModal` in the same way you would expect to present a `UIViewController`
@ -130,7 +147,7 @@ We will only be fixing critical bugs, thus, for any non-critical issues or featu
## Authors
[Stephen Sowole](https://github.com/tun57) • [Tosin Afolabi](https://github.com/tosinaf)
[Stephen Sowole](https://github.com/ste57) • [Tosin Afolabi](https://github.com/tosinaf)
## License

View File

@ -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 {

View File

@ -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 {

View File

@ -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? {

View File

@ -48,11 +48,13 @@ 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)