Compare commits

...

13 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
10 changed files with 191 additions and 47 deletions

View File

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

View File

@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'PanModal'
s.version = '1.2.7'
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,10 +18,10 @@ 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 = '5.0'

View File

@ -30,6 +30,7 @@ open class PanModalPresentationController: UIPresentationController {
*/
public enum PresentationState {
case shortForm
case mediumForm
case longForm
}
@ -79,11 +80,17 @@ open 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
*/
@ -105,15 +112,13 @@ open class PanModalPresentationController: UIPresentationController {
Background view used as an overlay over the presenting view
*/
private lazy var backgroundView: DimmedView = {
let view: DimmedView
let view: DimmedView = presentable?.dimmedView ?? DimmedView()
if let color = presentable?.panModalBackgroundColor {
view = DimmedView(dimColor: color)
} else {
view = DimmedView()
view.backgroundColor = color
}
view.didTap = { [weak self] _ in
if self?.presentable?.allowsTapToDismiss == true {
self?.presentedViewController.dismiss(animated: true)
self?.presentable?.onTapToDismiss?()
}
}
return view
@ -187,7 +192,9 @@ open 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()
})
}
@ -262,6 +269,10 @@ public extension PanModalPresentationController {
switch state {
case .shortForm:
snap(toYPosition: shortFormYPosition)
case .mediumForm:
snap(toYPosition: mediumFormYPosition)
case .longForm:
snap(toYPosition: longFormYPosition)
}
@ -370,8 +381,10 @@ private extension PanModalPresentationController {
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 ![shortFormYPosition, longFormYPosition].contains(panFrame.origin.y) {
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
@ -426,6 +439,7 @@ private extension PanModalPresentationController {
else { return }
shortFormYPosition = layoutPresentable.shortFormYPos
mediumFormYPosition = layoutPresentable.mediumFormYPos
longFormYPosition = layoutPresentable.longFormYPos
anchorModalToLongForm = layoutPresentable.anchorModalToLongForm
extendsPanScrolling = layoutPresentable.allowsExtendedPanScrolling
@ -454,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`
@ -518,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 {
presentedViewController.dismiss(animated: true)
presentable?.onDragToDismiss?()
}
} else {
@ -532,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 {
presentedViewController.dismiss(animated: true)
presentable?.onDragToDismiss?()
}
}
}
@ -653,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)
}
/**

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

@ -13,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
@ -31,6 +47,10 @@ public extension PanModalPresentable where Self: UIViewController {
return .contentHeight(scrollView.contentSize.height)
}
var dimmedView: DimmedView? {
DimmedView()
}
var cornerRadius: CGFloat {
return 8.0
}

View File

@ -63,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

View File

@ -45,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.
@ -55,6 +57,7 @@ public protocol PanModalPresentable: AnyObject {
*/
var longFormHeight: PanModalHeight { get }
var dimmedView: DimmedView? { get }
/**
The corner radius used when `shouldRoundTopCorners` is enabled.
@ -135,6 +138,10 @@ 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.

View File

@ -29,7 +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

@ -35,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`
@ -55,7 +59,7 @@ extension UIViewController: PanModalPresenter {
viewControllerToPresent.transitioningDelegate = PanModalPresentationDelegate.default
}
present(viewControllerToPresent, animated: true, completion: nil)
present(viewControllerToPresent, animated: true, completion: completion)
}
}

View File

@ -11,13 +11,13 @@ 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)
@ -30,21 +30,14 @@ public class DimmedView: UIView {
*/
var dimState: DimState = .off {
didSet {
switch dimState {
case .max:
alpha = 1.0
case .off:
alpha = 0.0
case .percent(let percentage):
alpha = max(0.0, min(1.0, percentage))
}
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
@ -55,10 +48,9 @@ public class DimmedView: UIView {
// MARK: - Initializers
init(dimColor: UIColor = UIColor.black.withAlphaComponent(0.7)) {
public init() {
super.init(frame: .zero)
alpha = 0.0
backgroundColor = dimColor
addGestureRecognizer(tapGesture)
}
@ -68,8 +60,21 @@ 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))
}
}
}