build: move included pan modal sources to separate dependency
This commit is contained in:
parent
27d5a3a9ca
commit
0ef1edfacb
|
|
@ -54,6 +54,15 @@
|
|||
"version" : "15.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "panmodal",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://git.svc.touchin.ru/TouchInstinct/PanModal",
|
||||
"state" : {
|
||||
"revision" : "be82eddb529faa2bc668230906ec007c53e7b635",
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "reactiveswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ let package = Package(
|
|||
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.4.0")),
|
||||
.package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "15.0.0")),
|
||||
.package(url: "https://github.com/hyperoslo/Cache.git", .upToNextMajor(from: "6.0.0")),
|
||||
.package(url: "https://github.com/antlr/antlr4", .upToNextMinor(from: "4.10.1"))
|
||||
.package(url: "https://github.com/antlr/antlr4", .upToNextMinor(from: "4.10.1")),
|
||||
.package(url: "https://git.svc.touchin.ru/TouchInstinct/PanModal", .upToNextMinor(from: "1.3.0"))
|
||||
],
|
||||
targets: [
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ let package = Package(
|
|||
|
||||
.target(name: "TIWebView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIWebView/Sources"),
|
||||
.target(name: "TIBottomSheet",
|
||||
dependencies: ["TIUIElements", "TIUIKitCore", "TISwiftUtils"],
|
||||
dependencies: ["PanModal", "TIUIElements", "TIUIKitCore", "TISwiftUtils"],
|
||||
path: "TIBottomSheet/Sources",
|
||||
exclude: ["../TIBottomSheet.app"],
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
ENV["DEVELOPMENT_INSTALL"] = "true"
|
||||
|
||||
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
|
||||
|
||||
target 'TIBottomSheet' do
|
||||
platform :ios, 11
|
||||
use_frameworks!
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import TISwiftUtils
|
|||
import TIUIElements
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
import PanModal
|
||||
|
||||
open class BaseModalViewController<ContentView: UIView,
|
||||
FooterContentView: UIView>: BaseInitializableViewController, PanModalPresentable {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import PanModal
|
||||
|
||||
public struct ModalViewPresentationDetent: Hashable {
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// Copyright (c) 2023 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 UIKit
|
||||
import PanModal
|
||||
|
||||
open class PassthroughDimmedView: DimmedView {
|
||||
weak var hitTestHandlerView: UIView?
|
||||
|
||||
public var isTransparent = false
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
guard let hitTestHandlerView else {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
return hitTestHandlerView.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
hitTestHandlerView == nil || super.point(inside: point, with: event)
|
||||
}
|
||||
|
||||
open override func onChange(dimState: DimmedView.DimState) {
|
||||
guard !isTransparent else {
|
||||
alpha = .zero
|
||||
return
|
||||
}
|
||||
|
||||
super.onChange(dimState: dimState)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
//
|
||||
// PanModalAnimator.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
Helper animation function to keep animations consistent.
|
||||
*/
|
||||
struct PanModalAnimator {
|
||||
|
||||
/**
|
||||
Constant Animation Properties
|
||||
*/
|
||||
struct Constants {
|
||||
static let defaultTransitionDuration: TimeInterval = 0.5
|
||||
}
|
||||
|
||||
static func animate(_ animations: @escaping PanModalPresentable.AnimationBlockType,
|
||||
config: PanModalPresentable?,
|
||||
_ completion: PanModalPresentable.AnimationCompletionType? = nil) {
|
||||
|
||||
let transitionDuration = config?.transitionDuration ?? Constants.defaultTransitionDuration
|
||||
let springDamping = config?.springDamping ?? 1.0
|
||||
let animationOptions = config?.transitionAnimationOptions ?? []
|
||||
|
||||
UIView.animate(withDuration: transitionDuration,
|
||||
delay: 0,
|
||||
usingSpringWithDamping: springDamping,
|
||||
initialSpringVelocity: 0,
|
||||
options: animationOptions,
|
||||
animations: animations,
|
||||
completion: completion)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
//
|
||||
// PanModalPresentationAnimator.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
Handles the animation of the presentedViewController as it is presented or dismissed.
|
||||
|
||||
This is a vertical animation that
|
||||
- Animates up from the bottom of the screen
|
||||
- Dismisses from the top to the bottom of the screen
|
||||
|
||||
This can be used as a standalone object for transition animation,
|
||||
but is primarily used in the PanModalPresentationDelegate for handling pan modal transitions.
|
||||
|
||||
- Note: The presentedViewController can conform to PanModalPresentable to adjust
|
||||
it's starting position through manipulating the shortFormHeight
|
||||
*/
|
||||
|
||||
public class PanModalPresentationAnimator: NSObject {
|
||||
|
||||
/**
|
||||
Enum representing the possible transition styles
|
||||
*/
|
||||
public enum TransitionStyle {
|
||||
case presentation
|
||||
case dismissal
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/**
|
||||
The transition style
|
||||
*/
|
||||
private let transitionStyle: TransitionStyle
|
||||
|
||||
/**
|
||||
Haptic feedback generator (during presentation)
|
||||
*/
|
||||
private var feedbackGenerator: UISelectionFeedbackGenerator?
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
required public init(transitionStyle: TransitionStyle) {
|
||||
self.transitionStyle = transitionStyle
|
||||
super.init()
|
||||
|
||||
/**
|
||||
Prepare haptic feedback, only during the presentation state
|
||||
*/
|
||||
if case .presentation = transitionStyle {
|
||||
feedbackGenerator = UISelectionFeedbackGenerator()
|
||||
feedbackGenerator?.prepare()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Animate presented view controller presentation
|
||||
*/
|
||||
private func animatePresentation(transitionContext: UIViewControllerContextTransitioning) {
|
||||
|
||||
guard
|
||||
let toVC = transitionContext.viewController(forKey: .to),
|
||||
let fromVC = transitionContext.viewController(forKey: .from)
|
||||
else { return }
|
||||
|
||||
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
|
||||
|
||||
// Use panView as presentingView if it already exists within the containerView
|
||||
let panView: UIView = transitionContext.containerView.panContainerView ?? toVC.view
|
||||
|
||||
// Move presented view offscreen (from the bottom)
|
||||
panView.frame = transitionContext.finalFrame(for: toVC)
|
||||
panView.frame.origin.y = transitionContext.containerView.frame.height
|
||||
|
||||
// Haptic feedback
|
||||
if presentable?.isHapticFeedbackEnabled == true {
|
||||
feedbackGenerator?.selectionChanged()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Animate presented view controller dismissal
|
||||
*/
|
||||
private func animateDismissal(transitionContext: UIViewControllerContextTransitioning) {
|
||||
|
||||
guard
|
||||
let toVC = transitionContext.viewController(forKey: .to),
|
||||
let fromVC = transitionContext.viewController(forKey: .from)
|
||||
else { return }
|
||||
|
||||
// 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
|
||||
|
||||
extension PanModalPresentationAnimator: UIViewControllerAnimatedTransitioning {
|
||||
|
||||
/**
|
||||
Returns the transition duration
|
||||
*/
|
||||
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
|
||||
guard
|
||||
let context = transitionContext,
|
||||
let presentable = panModalLayoutType(from: context)
|
||||
else { return PanModalAnimator.Constants.defaultTransitionDuration }
|
||||
|
||||
return presentable.transitionDuration
|
||||
}
|
||||
|
||||
/**
|
||||
Performs the appropriate animation based on the transition style
|
||||
*/
|
||||
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
switch transitionStyle {
|
||||
case .presentation:
|
||||
animatePresentation(transitionContext: transitionContext)
|
||||
case .dismissal:
|
||||
animateDismissal(transitionContext: transitionContext)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,881 +0,0 @@
|
|||
//
|
||||
// PanModalPresentationController.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
The PanModalPresentationController is the middle layer between the presentingViewController
|
||||
and the presentedViewController.
|
||||
|
||||
It controls the coordination between the individual transition classes as well as
|
||||
provides an abstraction over how the presented view is presented & displayed.
|
||||
|
||||
For example, we add a drag indicator view above the presented view and
|
||||
a background overlay between the presenting & presented view.
|
||||
|
||||
The presented view's layout configuration & presentation is defined using the PanModalPresentable.
|
||||
|
||||
By conforming to the PanModalPresentable protocol & overriding values
|
||||
the presented view can define its layout configuration & presentation.
|
||||
*/
|
||||
open class PanModalPresentationController: UIPresentationController {
|
||||
|
||||
/**
|
||||
Enum representing the possible presentation states
|
||||
*/
|
||||
public enum PresentationState {
|
||||
case shortForm
|
||||
case mediumForm
|
||||
case longForm
|
||||
}
|
||||
|
||||
/**
|
||||
Constants
|
||||
*/
|
||||
struct Constants {
|
||||
static let indicatorYOffset = CGFloat(8.0)
|
||||
static let snapMovementSensitivity = CGFloat(0.7)
|
||||
static let dragIndicatorSize = CGSize(width: 36.0, height: 5.0)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/**
|
||||
A flag to track if the presented view is animating
|
||||
*/
|
||||
private var isPresentedViewAnimating = false
|
||||
|
||||
/**
|
||||
A flag to determine if scrolling should seamlessly transition
|
||||
from the pan modal container view to the scroll view
|
||||
once the scroll limit has been reached.
|
||||
*/
|
||||
private var extendsPanScrolling = true
|
||||
|
||||
/**
|
||||
A flag to determine if scrolling should be limited to the longFormHeight.
|
||||
Return false to cap scrolling at .max height.
|
||||
*/
|
||||
private var anchorModalToLongForm = true
|
||||
|
||||
/**
|
||||
The y content offset value of the embedded scroll view
|
||||
*/
|
||||
private var scrollViewYOffset: CGFloat = 0.0
|
||||
|
||||
/**
|
||||
An observer for the scroll view content offset
|
||||
*/
|
||||
private var scrollObserver: NSKeyValueObservation?
|
||||
|
||||
// store the y positions so we don't have to keep re-calculating
|
||||
|
||||
/**
|
||||
The y value for the short form presentation state
|
||||
*/
|
||||
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
|
||||
*/
|
||||
private var anchoredYPosition: CGFloat {
|
||||
let defaultTopOffset = presentable?.topOffset ?? 0
|
||||
return anchorModalToLongForm ? longFormYPosition : defaultTopOffset
|
||||
}
|
||||
|
||||
/**
|
||||
Configuration object for PanModalPresentationController
|
||||
*/
|
||||
private var presentable: PanModalPresentable? {
|
||||
return presentedViewController as? PanModalPresentable
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
/**
|
||||
Background view used as an overlay over the presenting view
|
||||
*/
|
||||
private lazy var backgroundView: DimmedView = {
|
||||
let view: DimmedView
|
||||
let type = presentable?.dimmedViewType ?? .opaque
|
||||
|
||||
if let color = presentable?.panModalBackgroundColor {
|
||||
view = DimmedView(presentingController: presentingViewController, dimColor: color, appearanceType: type)
|
||||
} else {
|
||||
view = DimmedView(presentingController: presentingViewController, appearanceType: type)
|
||||
}
|
||||
|
||||
view.didTap = { [weak self] in
|
||||
self?.presentable?.onTapToDismiss?()
|
||||
}
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
/**
|
||||
A wrapper around the presented view so that we can modify
|
||||
the presented view apperance without changing
|
||||
the presented view's properties
|
||||
*/
|
||||
private lazy var panContainerView: PanContainerView = {
|
||||
let frame = containerView?.frame ?? .zero
|
||||
return PanContainerView(presentedView: presentedViewController.view, frame: frame)
|
||||
}()
|
||||
|
||||
/**
|
||||
Override presented view to return the pan container wrapper
|
||||
*/
|
||||
public override var presentedView: UIView {
|
||||
return panContainerView
|
||||
}
|
||||
|
||||
// MARK: - Gesture Recognizers
|
||||
|
||||
/**
|
||||
Gesture recognizer to detect & track pan gestures
|
||||
*/
|
||||
private lazy var panGestureRecognizer: UIPanGestureRecognizer = {
|
||||
let gesture = UIPanGestureRecognizer(target: self, action: #selector(didPanOnPresentedView(_ :)))
|
||||
gesture.minimumNumberOfTouches = 1
|
||||
gesture.maximumNumberOfTouches = 1
|
||||
gesture.delegate = self
|
||||
return gesture
|
||||
}()
|
||||
|
||||
// MARK: - Deinitializers
|
||||
|
||||
deinit {
|
||||
scrollObserver?.invalidate()
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override public func containerViewWillLayoutSubviews() {
|
||||
super.containerViewWillLayoutSubviews()
|
||||
configureViewLayout()
|
||||
}
|
||||
|
||||
override public func presentationTransitionWillBegin() {
|
||||
|
||||
guard let containerView = containerView
|
||||
else { return }
|
||||
|
||||
layoutBackgroundView(in: containerView)
|
||||
layoutPresentedView(in: containerView)
|
||||
configureScrollViewInsets()
|
||||
configureShadowIfNeeded()
|
||||
|
||||
guard let coordinator = presentedViewController.transitionCoordinator else {
|
||||
backgroundView.dimState = .max
|
||||
return
|
||||
}
|
||||
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
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
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
Drag indicator is drawn outside of view bounds
|
||||
so hiding it on view dismiss means avoiding visual bugs
|
||||
*/
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
self?.backgroundView.dimState = .off
|
||||
self?.presentingViewController.setNeedsStatusBarAppearanceUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
override public func dismissalTransitionDidEnd(_ completed: Bool) {
|
||||
if !completed { return }
|
||||
|
||||
presentable?.panModalDidDismiss()
|
||||
}
|
||||
|
||||
/**
|
||||
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
|
||||
|
||||
public extension PanModalPresentationController {
|
||||
|
||||
/**
|
||||
Transition the PanModalPresentationController
|
||||
to the given presentation state
|
||||
*/
|
||||
func transition(to state: PresentationState) {
|
||||
|
||||
guard presentable?.shouldTransition(to: state) == true
|
||||
else { return }
|
||||
|
||||
presentable?.willTransition(to: state)
|
||||
|
||||
switch state {
|
||||
case .shortForm:
|
||||
snap(toYPosition: shortFormYPosition)
|
||||
|
||||
case .mediumForm:
|
||||
snap(toYPosition: mediumFormYPosition)
|
||||
|
||||
case .longForm:
|
||||
snap(toYPosition: longFormYPosition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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 performUpdates(_ updates: () -> Void) {
|
||||
|
||||
guard let scrollView = presentable?.panScrollable
|
||||
else { return }
|
||||
|
||||
// Pause scroll observer
|
||||
scrollObserver?.invalidate()
|
||||
scrollObserver = nil
|
||||
|
||||
// Perform updates
|
||||
updates()
|
||||
|
||||
// Resume scroll observer
|
||||
trackScrolling(scrollView)
|
||||
observe(scrollView: scrollView)
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the PanModalPresentationController layout
|
||||
based on values in the PanModalPresentable
|
||||
|
||||
- Note: This should be called whenever any
|
||||
pan modal presentable value changes after the initial presentation
|
||||
*/
|
||||
func setNeedsLayoutUpdate() {
|
||||
configureViewLayout()
|
||||
adjustPresentedViewFrame()
|
||||
observe(scrollView: presentable?.panScrollable)
|
||||
configureScrollViewInsets()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Presented View Layout Configuration
|
||||
|
||||
private extension PanModalPresentationController {
|
||||
|
||||
/**
|
||||
Boolean flag to determine if the presented view is anchored
|
||||
*/
|
||||
var isPresentedViewAnchored: Bool {
|
||||
if !isPresentedViewAnimating
|
||||
&& extendsPanScrolling
|
||||
&& presentedView.frame.minY.rounded() <= anchoredYPosition.rounded() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
Adds the presented view to the given container view
|
||||
& configures the view elements such as drag indicator, rounded corners
|
||||
based on the pan modal presentable.
|
||||
*/
|
||||
func layoutPresentedView(in containerView: UIView) {
|
||||
|
||||
/**
|
||||
If the presented view controller does not conform to pan modal presentable
|
||||
don't configure
|
||||
*/
|
||||
guard let presentable = presentable
|
||||
else { return }
|
||||
|
||||
/**
|
||||
⚠️ If this class is NOT used in conjunction with the PanModalPresentationAnimator
|
||||
& PanModalPresentable, the presented view should be added to the container view
|
||||
in the presentation animator instead of here
|
||||
*/
|
||||
containerView.addSubview(presentedView)
|
||||
containerView.addGestureRecognizer(panGestureRecognizer)
|
||||
|
||||
if presentable.shouldRoundTopCorners {
|
||||
addRoundedCorners(to: presentedView)
|
||||
}
|
||||
|
||||
setNeedsLayoutUpdate()
|
||||
adjustPanContainerBackgroundColor()
|
||||
}
|
||||
|
||||
/**
|
||||
Reduce height of presentedView so that it sits at the bottom of the screen
|
||||
*/
|
||||
func adjustPresentedViewFrame() {
|
||||
|
||||
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, mediumFormYPosition, 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)
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
Adds a background color to the pan container view
|
||||
in order to avoid a gap at the bottom
|
||||
during initial view presentation in longForm (when view bounces)
|
||||
*/
|
||||
func adjustPanContainerBackgroundColor() {
|
||||
panContainerView.backgroundColor = presentedViewController.view.backgroundColor
|
||||
?? presentable?.panScrollable?.backgroundColor
|
||||
}
|
||||
|
||||
/**
|
||||
Adds the background view to the view hierarchy
|
||||
& configures its layout constraints.
|
||||
*/
|
||||
func layoutBackgroundView(in containerView: UIView) {
|
||||
containerView.addSubview(backgroundView)
|
||||
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
backgroundView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
|
||||
backgroundView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
|
||||
backgroundView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
|
||||
backgroundView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
|
||||
}
|
||||
|
||||
/**
|
||||
Calculates & stores the layout anchor points & options
|
||||
*/
|
||||
func configureViewLayout() {
|
||||
|
||||
guard let layoutPresentable = presentedViewController as? PanModalPresentable.LayoutType
|
||||
else { return }
|
||||
|
||||
shortFormYPosition = layoutPresentable.shortFormYPos
|
||||
mediumFormYPosition = layoutPresentable.mediumFormYPos
|
||||
longFormYPosition = layoutPresentable.longFormYPos
|
||||
anchorModalToLongForm = layoutPresentable.anchorModalToLongForm
|
||||
extendsPanScrolling = layoutPresentable.allowsExtendedPanScrolling
|
||||
|
||||
containerView?.isUserInteractionEnabled = layoutPresentable.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
Configures the scroll view insets
|
||||
*/
|
||||
func configureScrollViewInsets() {
|
||||
|
||||
guard
|
||||
let scrollView = presentable?.panScrollable,
|
||||
!scrollView.isScrolling
|
||||
else { return }
|
||||
|
||||
/**
|
||||
Disable vertical scroll indicator until we start to scroll
|
||||
to avoid visual bugs
|
||||
*/
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.scrollIndicatorInsets = presentable?.scrollIndicatorInsets ?? .zero
|
||||
|
||||
/**
|
||||
Set the appropriate contentInset as the configuration within this class
|
||||
offsets it
|
||||
*/
|
||||
scrollView.contentInset.bottom = presentingViewController.bottomLayoutGuide.length
|
||||
|
||||
/**
|
||||
As we adjust the bounds during `handleScrollViewTopBounce`
|
||||
we should assume that contentInsetAdjustmentBehavior will not be correct
|
||||
*/
|
||||
if #available(iOS 11.0, *) {
|
||||
scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
}
|
||||
|
||||
func configureShadowIfNeeded() {
|
||||
let type = presentable?.dimmedViewType ?? .opaque
|
||||
|
||||
if case let .transparentWithShadow(shadow) = type {
|
||||
containerView?.configureUIView(appearance: UIView.DefaultAppearance(shadow: shadow))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Pan Gesture Event Handler
|
||||
|
||||
private extension PanModalPresentationController {
|
||||
|
||||
/**
|
||||
The designated function for handling pan gesture events
|
||||
*/
|
||||
@objc func didPanOnPresentedView(_ recognizer: UIPanGestureRecognizer) {
|
||||
|
||||
guard
|
||||
shouldRespond(to: recognizer),
|
||||
let containerView = containerView
|
||||
else {
|
||||
recognizer.setTranslation(.zero, in: recognizer.view)
|
||||
return
|
||||
}
|
||||
|
||||
switch recognizer.state {
|
||||
case .began, .changed:
|
||||
|
||||
/**
|
||||
Respond accordingly to pan gesture translation
|
||||
*/
|
||||
respond(to: recognizer)
|
||||
|
||||
/**
|
||||
If presentedView is translated above the longForm threshold, treat as transition
|
||||
*/
|
||||
if presentedView.frame.origin.y == anchoredYPosition && extendsPanScrolling {
|
||||
presentable?.willTransition(to: .longForm)
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
/**
|
||||
Use velocity sensitivity value to restrict snapping
|
||||
*/
|
||||
let velocity = recognizer.velocity(in: presentedView)
|
||||
|
||||
if isVelocityWithinSensitivityRange(velocity.y) {
|
||||
|
||||
/**
|
||||
If velocity is within the sensitivity range,
|
||||
transition to a presentation state or dismiss entirely.
|
||||
|
||||
This allows the user to dismiss directly from long form
|
||||
instead of going to the short form state first.
|
||||
*/
|
||||
if velocity.y < 0 {
|
||||
transition(to: .longForm)
|
||||
|
||||
} 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 {
|
||||
presentable?.onDragToDismiss?()
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
/**
|
||||
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, mediumFormYPosition, longFormYPosition])
|
||||
|
||||
if position == longFormYPosition {
|
||||
transition(to: .longForm)
|
||||
|
||||
} else if position == mediumFormYPosition {
|
||||
transition(to: .mediumForm)
|
||||
|
||||
} else if position == shortFormYPosition || allowsDragToDismiss == false {
|
||||
transition(to: .shortForm)
|
||||
|
||||
} else {
|
||||
presentable?.onDragToDismiss?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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
|
||||
*/
|
||||
func respond(to panGestureRecognizer: UIPanGestureRecognizer) {
|
||||
presentable?.willRespond(to: panGestureRecognizer)
|
||||
|
||||
var yDisplacement = panGestureRecognizer.translation(in: presentedView).y
|
||||
|
||||
/**
|
||||
If the presentedView is not anchored to long form, reduce the rate of movement
|
||||
above the threshold
|
||||
*/
|
||||
if presentedView.frame.origin.y < longFormYPosition {
|
||||
yDisplacement /= 2.0
|
||||
}
|
||||
adjust(toYPosition: presentedView.frame.origin.y + yDisplacement)
|
||||
|
||||
panGestureRecognizer.setTranslation(.zero, in: presentedView)
|
||||
}
|
||||
|
||||
/**
|
||||
Determines if we should fail the gesture recognizer based on certain conditions
|
||||
|
||||
We fail the presented view's pan gesture recognizer if we are actively scrolling on the scroll view.
|
||||
This allows the user to drag whole view controller from outside scrollView touch area.
|
||||
|
||||
Unfortunately, cancelling a gestureRecognizer means that we lose the effect of transition scrolling
|
||||
from one view to another in the same pan gesture so don't cancel
|
||||
*/
|
||||
func shouldFail(panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
|
||||
|
||||
/**
|
||||
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 the panScrollable recognizer,
|
||||
for the purpose of ensuring we're no longer tracking the scrollView
|
||||
*/
|
||||
guard !shouldPrioritize(panGestureRecognizer: panGestureRecognizer) else {
|
||||
presentable?.panScrollable?.panGestureRecognizer.isEnabled = false
|
||||
presentable?.panScrollable?.panGestureRecognizer.isEnabled = true
|
||||
return false
|
||||
}
|
||||
|
||||
guard
|
||||
isPresentedViewAnchored,
|
||||
let scrollView = presentable?.panScrollable,
|
||||
scrollView.contentOffset.y > 0
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
let loc = panGestureRecognizer.location(in: presentedView)
|
||||
return (scrollView.frame.contains(loc) || scrollView.isScrolling)
|
||||
}
|
||||
|
||||
/**
|
||||
Determine if the presented view's panGestureRecognizer should be prioritized over
|
||||
embedded scrollView's panGestureRecognizer.
|
||||
*/
|
||||
func shouldPrioritize(panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
|
||||
return panGestureRecognizer.state == .began &&
|
||||
presentable?.shouldPrioritize(panModalGestureRecognizer: panGestureRecognizer) == true
|
||||
}
|
||||
|
||||
/**
|
||||
Check if the given velocity is within the sensitivity range
|
||||
*/
|
||||
func isVelocityWithinSensitivityRange(_ velocity: CGFloat) -> Bool {
|
||||
return (abs(velocity) - (1000 * (1 - Constants.snapMovementSensitivity))) > 0
|
||||
}
|
||||
|
||||
func snap(toYPosition yPos: CGFloat) {
|
||||
PanModalAnimator.animate({ [weak self] in
|
||||
self?.adjust(toYPosition: yPos)
|
||||
self?.isPresentedViewAnimating = true
|
||||
}, config: presentable) { [weak self] didComplete in
|
||||
self?.isPresentedViewAnimating = !didComplete
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the y position of the presentedView & adjusts the backgroundView.
|
||||
*/
|
||||
func adjust(toYPosition yPos: CGFloat) {
|
||||
presentedView.frame.origin.y = max(yPos, anchoredYPosition)
|
||||
|
||||
let maxHeight = UIScreen.main.bounds.height - longFormYPosition
|
||||
|
||||
backgroundView.dimState = .percent(1 - presentedView.frame.origin.y / maxHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
Finds the nearest value to a given number out of a given array of float values
|
||||
|
||||
- Parameters:
|
||||
- number: reference float we are trying to find the closest value to
|
||||
- values: array of floats we would like to compare against
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIScrollView Observer
|
||||
|
||||
private extension PanModalPresentationController {
|
||||
|
||||
/**
|
||||
Creates & stores an observer on the given scroll view's content offset.
|
||||
This allows us to track scrolling without overriding the scrollView delegate
|
||||
*/
|
||||
func observe(scrollView: UIScrollView?) {
|
||||
scrollObserver?.invalidate()
|
||||
scrollObserver = scrollView?.observe(\.contentOffset, options: .old) { [weak self] scrollView, change in
|
||||
|
||||
/**
|
||||
Incase we have a situation where we have two containerViews in the same presentation
|
||||
*/
|
||||
guard self?.containerView != nil
|
||||
else { return }
|
||||
|
||||
self?.didPanOnScrollView(scrollView, change: change)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Scroll view content offset change event handler
|
||||
|
||||
Also when scrollView is scrolled to the top, we disable the scroll indicator
|
||||
otherwise glitchy behaviour occurs
|
||||
|
||||
This is also shown in Apple Maps (reverse engineering)
|
||||
which allows us to seamlessly transition scrolling from the panContainerView to the scrollView
|
||||
*/
|
||||
func didPanOnScrollView(_ scrollView: UIScrollView, change: NSKeyValueObservedChange<CGPoint>) {
|
||||
|
||||
guard
|
||||
!presentedViewController.isBeingDismissed,
|
||||
!presentedViewController.isBeingPresented
|
||||
else { return }
|
||||
|
||||
if !isPresentedViewAnchored && scrollView.contentOffset.y > 0 {
|
||||
|
||||
/**
|
||||
Hold the scrollView in place if we're actively scrolling and not handling top bounce
|
||||
*/
|
||||
haltScrolling(scrollView)
|
||||
|
||||
} else if scrollView.isScrolling || isPresentedViewAnimating {
|
||||
|
||||
if isPresentedViewAnchored {
|
||||
/**
|
||||
While we're scrolling upwards on the scrollView,
|
||||
store the last content offset position
|
||||
*/
|
||||
trackScrolling(scrollView)
|
||||
} else {
|
||||
/**
|
||||
Keep scroll view in place while we're panning on main view
|
||||
*/
|
||||
haltScrolling(scrollView)
|
||||
}
|
||||
|
||||
} else if presentedViewController.view.isKind(of: UIScrollView.self)
|
||||
&& !isPresentedViewAnimating && scrollView.contentOffset.y <= 0 {
|
||||
|
||||
/**
|
||||
In the case where we drag down quickly on the scroll view and let go,
|
||||
`handleScrollViewTopBounce` adds a nice elegant touch.
|
||||
*/
|
||||
handleScrollViewTopBounce(scrollView: scrollView, change: change)
|
||||
} else {
|
||||
trackScrolling(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Halts the scroll of a given scroll view & anchors it at the `scrollViewYOffset`
|
||||
*/
|
||||
func haltScrolling(_ scrollView: UIScrollView) {
|
||||
scrollView.setContentOffset(CGPoint(x: 0, y: scrollViewYOffset), animated: false)
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
}
|
||||
|
||||
/**
|
||||
As the user scrolls, track & save the scroll view y offset.
|
||||
This helps halt scrolling when we want to hold the scroll view in place.
|
||||
*/
|
||||
func trackScrolling(_ scrollView: UIScrollView) {
|
||||
scrollViewYOffset = max(scrollView.contentOffset.y, 0)
|
||||
scrollView.showsVerticalScrollIndicator = true
|
||||
}
|
||||
|
||||
/**
|
||||
To ensure that the scroll transition between the scrollView & the modal
|
||||
is completely seamless, we need to handle the case where content offset is negative.
|
||||
|
||||
In this case, we follow the curve of the decelerating scroll view.
|
||||
This gives the effect that the modal view and the scroll view are one view entirely.
|
||||
|
||||
- Note: This works best where the view behind view controller is a UIScrollView.
|
||||
So, for example, a UITableViewController.
|
||||
*/
|
||||
func handleScrollViewTopBounce(scrollView: UIScrollView, change: NSKeyValueObservedChange<CGPoint>) {
|
||||
|
||||
guard let oldYValue = change.oldValue?.y, scrollView.isDecelerating
|
||||
else { return }
|
||||
|
||||
let yOffset = scrollView.contentOffset.y
|
||||
let presentedSize = containerView?.frame.size ?? .zero
|
||||
|
||||
/**
|
||||
Decrease the view bounds by the y offset so the scroll view stays in place
|
||||
and we can still get updates on its content offset
|
||||
*/
|
||||
presentedView.bounds.size = CGSize(width: presentedSize.width, height: presentedSize.height + yOffset)
|
||||
|
||||
if oldYValue > yOffset {
|
||||
/**
|
||||
Move the view in the opposite direction to the decreasing bounds
|
||||
until half way through the deceleration so that it appears
|
||||
as if we're transferring the scrollView drag momentum to the entire view
|
||||
*/
|
||||
presentedView.frame.origin.y = longFormYPosition - yOffset
|
||||
} else {
|
||||
scrollViewYOffset = 0
|
||||
snap(toYPosition: longFormYPosition)
|
||||
}
|
||||
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
|
||||
extension PanModalPresentationController: UIGestureRecognizerDelegate {
|
||||
|
||||
/**
|
||||
Do not require any other gesture recognizers to fail
|
||||
*/
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
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.view == presentable?.panScrollable
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIBezierPath
|
||||
|
||||
private extension PanModalPresentationController {
|
||||
|
||||
/**
|
||||
Draws top rounded corners on a given view
|
||||
We have to set a custom path for corner rounding
|
||||
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))
|
||||
|
||||
// Set path as a mask to display optional drag indicator view & rounded corners
|
||||
let mask = CAShapeLayer()
|
||||
mask.path = path.cgPath
|
||||
view.layer.mask = mask
|
||||
|
||||
// Improve performance by rasterizing the layer
|
||||
view.layer.shouldRasterize = true
|
||||
view.layer.rasterizationScale = UIScreen.main.scale
|
||||
}
|
||||
|
||||
/**
|
||||
Draws a path around the drag indicator view
|
||||
*/
|
||||
func drawAroundDragIndicator(currentPath path: UIBezierPath, indicatorLeftEdgeXPos: CGFloat) {
|
||||
|
||||
let totalIndicatorOffset = Constants.indicatorYOffset + Constants.dragIndicatorSize.height
|
||||
|
||||
// Draw around drag indicator starting from the left
|
||||
path.addLine(to: CGPoint(x: indicatorLeftEdgeXPos, y: path.currentPoint.y))
|
||||
path.addLine(to: CGPoint(x: path.currentPoint.x, y: path.currentPoint.y - totalIndicatorOffset))
|
||||
path.addLine(to: CGPoint(x: path.currentPoint.x + Constants.dragIndicatorSize.width, y: path.currentPoint.y))
|
||||
path.addLine(to: CGPoint(x: path.currentPoint.x, y: path.currentPoint.y + totalIndicatorOffset))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Extensions
|
||||
|
||||
private extension UIScrollView {
|
||||
|
||||
/**
|
||||
A flag to determine if a scroll view is scrolling
|
||||
*/
|
||||
var isScrolling: Bool {
|
||||
return isDragging && !isDecelerating || isTracking
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
//
|
||||
// PanModalPresentationDelegate.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
The PanModalPresentationDelegate conforms to the various transition delegates
|
||||
and vends the appropriate object for each transition controller requested.
|
||||
|
||||
Usage:
|
||||
```
|
||||
viewController.modalPresentationStyle = .custom
|
||||
viewController.transitioningDelegate = PanModalPresentationDelegate.default
|
||||
```
|
||||
*/
|
||||
public class PanModalPresentationDelegate: NSObject {
|
||||
|
||||
/**
|
||||
Returns an instance of the delegate, retained for the duration of presentation
|
||||
*/
|
||||
public static var `default`: PanModalPresentationDelegate = {
|
||||
return PanModalPresentationDelegate()
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
extension PanModalPresentationDelegate: UIViewControllerTransitioningDelegate {
|
||||
|
||||
/**
|
||||
Returns a modal presentation animator configured for the presenting state
|
||||
*/
|
||||
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
return PanModalPresentationAnimator(transitionStyle: .presentation)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a modal presentation animator configured for the dismissing state
|
||||
*/
|
||||
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
return PanModalPresentationAnimator(transitionStyle: .dismissal)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a modal presentation controller to coordinate the transition from the presenting
|
||||
view controller to the presented view controller.
|
||||
|
||||
Changes in size class during presentation are handled via the adaptive presentation delegate
|
||||
*/
|
||||
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
|
||||
let controller = PanModalPresentationController(presentedViewController: presented, presenting: presenting)
|
||||
controller.delegate = self
|
||||
return controller
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PanModalPresentationDelegate: UIAdaptivePresentationControllerDelegate, UIPopoverPresentationControllerDelegate {
|
||||
|
||||
/**
|
||||
- Note: We do not adapt to size classes due to the introduction of the UIPresentationController
|
||||
& deprecation of UIPopoverController (iOS 9), there is no way to have more than one
|
||||
presentation controller in use during the same presentation
|
||||
|
||||
This is essential when transitioning from .popover to .custom on iPad split view... unless a custom popover view is also implemented
|
||||
(popover uses UIPopoverPresentationController & we use PanModalPresentationController)
|
||||
*/
|
||||
|
||||
/**
|
||||
Dismisses the presented view controller
|
||||
*/
|
||||
public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
return .none
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
Copyright © 2018 Tiny Speck, Inc.
|
||||
|
||||
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.
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// PanModal.h
|
||||
// PanModal
|
||||
//
|
||||
// Created by Tosin A on 3/13/19.
|
||||
// Copyright © 2019 Detail. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
//! Project version number for PanModal.
|
||||
FOUNDATION_EXPORT double PanModalVersionNumber;
|
||||
|
||||
//! Project version string for PanModal.
|
||||
FOUNDATION_EXPORT const unsigned char PanModalVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <PanModal/PublicHeader.h>
|
||||
|
||||
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
//
|
||||
// PanModalHeight.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
An enum that defines the possible states of the height of a pan modal container view
|
||||
for a given presentation state (shortForm, longForm)
|
||||
*/
|
||||
public enum PanModalHeight: Equatable {
|
||||
|
||||
/**
|
||||
Sets the height to be the maximum height (+ topOffset)
|
||||
*/
|
||||
case maxHeight
|
||||
|
||||
/**
|
||||
Sets the height to be the max height with a specified top inset.
|
||||
- Note: A value of 0 is equivalent to .maxHeight
|
||||
*/
|
||||
case maxHeightWithTopInset(CGFloat)
|
||||
|
||||
/**
|
||||
Sets the height to be the specified content height
|
||||
*/
|
||||
case contentHeight(CGFloat)
|
||||
|
||||
/**
|
||||
Sets the height to be the specified content height
|
||||
& also ignores the bottomSafeAreaInset
|
||||
*/
|
||||
case contentHeightIgnoringSafeArea(CGFloat)
|
||||
|
||||
/**
|
||||
Sets the height to be the intrinsic content height
|
||||
*/
|
||||
case intrinsicHeight
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
//
|
||||
// PanModalPresentable+Defaults.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2018 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import TISwiftUtils
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
Default values for the PanModalPresentable.
|
||||
*/
|
||||
public extension PanModalPresentable where Self: UIViewController {
|
||||
|
||||
var onTapToDismiss: VoidClosure? {
|
||||
{ [weak self] in
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
var onDragToDismiss: VoidClosure? {
|
||||
{ [weak self] in
|
||||
self?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
var topOffset: CGFloat {
|
||||
topLayoutOffset
|
||||
}
|
||||
|
||||
var shortFormHeight: PanModalHeight {
|
||||
return longFormHeight
|
||||
}
|
||||
|
||||
var mediumFormHeight: PanModalHeight {
|
||||
longFormHeight
|
||||
}
|
||||
|
||||
var longFormHeight: PanModalHeight {
|
||||
|
||||
guard let scrollView = panScrollable
|
||||
else { return .maxHeight }
|
||||
|
||||
// called once during presentation and stored
|
||||
scrollView.layoutIfNeeded()
|
||||
return .contentHeight(scrollView.contentSize.height)
|
||||
}
|
||||
|
||||
var dimmedViewType: DimmedView.AppearanceType {
|
||||
.opaque
|
||||
}
|
||||
|
||||
var presentationDetents: [ModalViewPresentationDetent] {
|
||||
[]
|
||||
}
|
||||
|
||||
var cornerRadius: CGFloat {
|
||||
return 8.0
|
||||
}
|
||||
|
||||
var springDamping: CGFloat {
|
||||
return 0.8
|
||||
}
|
||||
|
||||
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 scrollIndicatorInsets: UIEdgeInsets {
|
||||
let top = shouldRoundTopCorners ? cornerRadius : 0
|
||||
return UIEdgeInsets(top: CGFloat(top), left: 0, bottom: bottomLayoutOffset, right: 0)
|
||||
}
|
||||
|
||||
var anchorModalToLongForm: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var allowsExtendedPanScrolling: Bool {
|
||||
|
||||
guard let scrollView = panScrollable
|
||||
else { return false }
|
||||
|
||||
scrollView.layoutIfNeeded()
|
||||
return scrollView.contentSize.height > (scrollView.frame.height - bottomLayoutOffset)
|
||||
}
|
||||
|
||||
var allowsDragToDismiss: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var allowsTapToDismiss: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var isUserInteractionEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var isHapticFeedbackEnabled: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var shouldRoundTopCorners: Bool {
|
||||
return isPanModalPresented
|
||||
}
|
||||
|
||||
func shouldRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func willRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) {
|
||||
|
||||
}
|
||||
|
||||
func shouldTransition(to state: PanModalPresentationController.PresentationState) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func shouldPrioritize(panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func willTransition(to state: PanModalPresentationController.PresentationState) {
|
||||
|
||||
}
|
||||
|
||||
func panModalWillDismiss() {
|
||||
|
||||
}
|
||||
|
||||
func panModalDidDismiss() {
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
//
|
||||
// PanModalPresentable+LayoutHelpers.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2018 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
⚠️ [Internal Only] ⚠️
|
||||
Helper extensions that handle layout in the PanModalPresentationController
|
||||
*/
|
||||
extension PanModalPresentable where Self: UIViewController {
|
||||
|
||||
/**
|
||||
Cast the presentation controller to PanModalPresentationController
|
||||
so we can access PanModalPresentationController properties and methods
|
||||
*/
|
||||
var presentedVC: PanModalPresentationController? {
|
||||
return presentationController as? PanModalPresentationController
|
||||
}
|
||||
|
||||
/**
|
||||
Length of the top layout guide of the presenting view controller.
|
||||
Gives us the safe area inset from the top.
|
||||
*/
|
||||
var topLayoutOffset: CGFloat {
|
||||
|
||||
guard let rootVC = rootViewController
|
||||
else { return 0}
|
||||
|
||||
if #available(iOS 11.0, *) { return rootVC.view.safeAreaInsets.top } else { return rootVC.topLayoutGuide.length }
|
||||
}
|
||||
|
||||
/**
|
||||
Length of the bottom layout guide of the presenting view controller.
|
||||
Gives us the safe area inset from the bottom.
|
||||
*/
|
||||
var bottomLayoutOffset: CGFloat {
|
||||
|
||||
guard let rootVC = rootViewController
|
||||
else { return 0}
|
||||
|
||||
if #available(iOS 11.0, *) { return rootVC.view.safeAreaInsets.bottom } else { return rootVC.bottomLayoutGuide.length }
|
||||
}
|
||||
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
var shortFormYPos: CGFloat {
|
||||
|
||||
guard !UIAccessibility.isVoiceOverRunning
|
||||
else { return longFormYPos }
|
||||
|
||||
let shortFormYPos = topMargin(from: shortFormHeight) + topOffset
|
||||
|
||||
// shortForm shouldn't exceed longForm
|
||||
return max(shortFormYPos, longFormYPos)
|
||||
}
|
||||
|
||||
var mediumFormYPos: CGFloat {
|
||||
let mediumFormYPos = topMargin(from: mediumFormHeight)
|
||||
|
||||
return max(mediumFormYPos, longFormYPos)
|
||||
}
|
||||
|
||||
/**
|
||||
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
|
||||
*/
|
||||
var longFormYPos: CGFloat {
|
||||
return max(topMargin(from: longFormHeight), topMargin(from: .maxHeight)) + topOffset
|
||||
}
|
||||
|
||||
/**
|
||||
Use the container view for relative positioning as this view's frame
|
||||
is adjusted in PanModalPresentationController
|
||||
*/
|
||||
var bottomYPos: CGFloat {
|
||||
|
||||
guard let container = presentedVC?.containerView
|
||||
else { return view.bounds.height }
|
||||
|
||||
return container.bounds.size.height - topOffset
|
||||
}
|
||||
|
||||
/**
|
||||
Converts a given pan modal height value into a y position value
|
||||
calculated from top of view
|
||||
*/
|
||||
func topMargin(from: PanModalHeight) -> CGFloat {
|
||||
switch from {
|
||||
case .maxHeight:
|
||||
return 0.0
|
||||
case .maxHeightWithTopInset(let inset):
|
||||
return inset
|
||||
case .contentHeight(let height):
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private var rootViewController: UIViewController? {
|
||||
|
||||
guard let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication
|
||||
else { return nil }
|
||||
|
||||
return application.keyWindow?.rootViewController
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
//
|
||||
// PanModalPresentable+UIViewController.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2018 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
Extends PanModalPresentable with helper methods
|
||||
when the conforming object is a UIViewController
|
||||
*/
|
||||
public extension PanModalPresentable where Self: UIViewController {
|
||||
|
||||
typealias AnimationBlockType = () -> Void
|
||||
typealias AnimationCompletionType = (Bool) -> Void
|
||||
|
||||
/**
|
||||
For Presentation, the object must be a UIViewController & confrom to the PanModalPresentable protocol.
|
||||
*/
|
||||
typealias LayoutType = UIViewController & PanModalPresentable
|
||||
|
||||
/**
|
||||
A function wrapper over the `transition(to state: PanModalPresentationController.PresentationState)`
|
||||
function in the PanModalPresentationController.
|
||||
*/
|
||||
func panModalTransition(to state: PanModalPresentationController.PresentationState) {
|
||||
presentedVC?.transition(to: state)
|
||||
}
|
||||
|
||||
/**
|
||||
A function wrapper over the `setNeedsLayoutUpdate()`
|
||||
function in the PanModalPresentationController.
|
||||
|
||||
- Note: This should be called whenever any of the values for the PanModalPresentable protocol are changed.
|
||||
*/
|
||||
func panModalSetNeedsLayoutUpdate() {
|
||||
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.
|
||||
|
||||
This can be used for animation consistency on views within the presented view controller.
|
||||
*/
|
||||
func panModalAnimate(_ animationBlock: @escaping AnimationBlockType, _ completion: AnimationCompletionType? = nil) {
|
||||
PanModalAnimator.animate(animationBlock, config: self, completion)
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
//
|
||||
// PanModalPresentable.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2017 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import TISwiftUtils
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
This is the configuration object for a view controller
|
||||
that will be presented using the PanModal transition.
|
||||
|
||||
Usage:
|
||||
```
|
||||
extension YourViewController: PanModalPresentable {
|
||||
func shouldRoundTopCorners: Bool { return false }
|
||||
}
|
||||
```
|
||||
*/
|
||||
public protocol PanModalPresentable: AnyObject {
|
||||
|
||||
/**
|
||||
The scroll view embedded in the view controller.
|
||||
Setting this value allows for seamless transition scrolling between the embedded scroll view
|
||||
and the pan modal container view.
|
||||
*/
|
||||
var panScrollable: UIScrollView? { get }
|
||||
|
||||
/**
|
||||
The offset between the top of the screen and the top of the pan modal container view.
|
||||
|
||||
Default value is the topLayoutGuide.length + 21.0.
|
||||
*/
|
||||
var topOffset: CGFloat { get }
|
||||
|
||||
/**
|
||||
The height of the pan modal container view
|
||||
when in the shortForm presentation state.
|
||||
|
||||
This value is capped to .max, if provided value exceeds the space available.
|
||||
|
||||
Default value is the longFormHeight.
|
||||
*/
|
||||
var shortFormHeight: PanModalHeight { get }
|
||||
|
||||
var mediumFormHeight: PanModalHeight { get }
|
||||
|
||||
/**
|
||||
The height of the pan modal container view
|
||||
when in the longForm presentation state.
|
||||
|
||||
This value is capped to .max, if provided value exceeds the space available.
|
||||
|
||||
Default value is .max.
|
||||
*/
|
||||
var longFormHeight: PanModalHeight { get }
|
||||
|
||||
var presentationDetents: [ModalViewPresentationDetent] { get }
|
||||
|
||||
var dimmedViewType: DimmedView.AppearanceType { 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.
|
||||
|
||||
Default Value is 0.8.
|
||||
*/
|
||||
var springDamping: CGFloat { get }
|
||||
|
||||
/**
|
||||
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 black with alpha component 0.7.
|
||||
*/
|
||||
var panModalBackgroundColor: UIColor { get }
|
||||
|
||||
/**
|
||||
We configure the panScrollable's scrollIndicatorInsets interally so override this value
|
||||
to set custom insets.
|
||||
|
||||
- Note: Use `panModalSetNeedsLayoutUpdate()` when updating insets.
|
||||
*/
|
||||
var scrollIndicatorInsets: UIEdgeInsets { get }
|
||||
|
||||
/**
|
||||
A flag to determine if scrolling should be limited to the longFormHeight.
|
||||
Return false to cap scrolling at .max height.
|
||||
|
||||
Default value is true.
|
||||
*/
|
||||
var anchorModalToLongForm: Bool { get }
|
||||
|
||||
/**
|
||||
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 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 }
|
||||
var onTapToDismiss: VoidClosure? { get }
|
||||
var onDragToDismiss: VoidClosure? { 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.
|
||||
|
||||
- Note: Return false to forward touches to the presentingViewController.
|
||||
|
||||
Default is true.
|
||||
*/
|
||||
var isUserInteractionEnabled: Bool { get }
|
||||
|
||||
/**
|
||||
A flag to determine if haptic feedback should be enabled during presentation.
|
||||
|
||||
Default value is true.
|
||||
*/
|
||||
var isHapticFeedbackEnabled: Bool { get }
|
||||
|
||||
/**
|
||||
A flag to determine if the top corners should be rounded.
|
||||
|
||||
Default value is true.
|
||||
*/
|
||||
var shouldRoundTopCorners: 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
|
||||
for the gesture recognizer state change.
|
||||
|
||||
For example, when the pan modal view is about to scroll.
|
||||
|
||||
Default value is an empty implementation.
|
||||
*/
|
||||
func willRespond(to panModalGestureRecognizer: UIPanGestureRecognizer)
|
||||
|
||||
/**
|
||||
Asks the delegate if the pan modal gesture recognizer should be prioritized.
|
||||
|
||||
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 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.
|
||||
*/
|
||||
func shouldPrioritize(panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool
|
||||
|
||||
/**
|
||||
Asks the delegate if the pan modal should transition to a new state.
|
||||
|
||||
Default value is true.
|
||||
*/
|
||||
func shouldTransition(to state: PanModalPresentationController.PresentationState) -> Bool
|
||||
|
||||
/**
|
||||
Notifies the delegate that the pan modal is about to transition to a new state.
|
||||
|
||||
Default value is an empty implementation.
|
||||
*/
|
||||
func willTransition(to state: PanModalPresentationController.PresentationState)
|
||||
|
||||
/**
|
||||
Notifies the delegate that the pan modal is about to be dismissed.
|
||||
|
||||
Default value is an empty implementation.
|
||||
*/
|
||||
func panModalWillDismiss()
|
||||
|
||||
/**
|
||||
Notifies the delegate after the pan modal is dismissed.
|
||||
|
||||
Default value is an empty implementation.
|
||||
*/
|
||||
func panModalDidDismiss()
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
//
|
||||
// PanModalPresenter.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
A protocol for objects that will present a view controller as a PanModal
|
||||
|
||||
- Usage:
|
||||
```
|
||||
viewController.presentPanModal(viewControllerToPresent: presentingVC,
|
||||
sourceView: presentingVC.view,
|
||||
sourceRect: .zero)
|
||||
```
|
||||
*/
|
||||
protocol PanModalPresenter: AnyObject {
|
||||
|
||||
/**
|
||||
A flag that returns true if the current presented view controller
|
||||
is using the PanModalPresentationDelegate
|
||||
*/
|
||||
var isPanModalPresented: Bool { get }
|
||||
|
||||
/**
|
||||
Presents a view controller that conforms to the PanModalPresentable protocol
|
||||
*/
|
||||
func presentPanModal(_ viewControllerToPresent: PanModalPresentable.LayoutType,
|
||||
sourceView: UIView?,
|
||||
sourceRect: CGRect,
|
||||
completion: (() -> Void)?)
|
||||
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
//
|
||||
// UIViewController+PanModalPresenterProtocol.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2019 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
Extends the UIViewController to conform to the PanModalPresenter protocol
|
||||
*/
|
||||
extension UIViewController: PanModalPresenter {
|
||||
|
||||
/**
|
||||
A flag that returns true if the topmost view controller in the navigation stack
|
||||
was presented using the custom PanModal transition
|
||||
|
||||
- Warning: ⚠️ Calling `presentationController` in this function may cause a memory leak. ⚠️
|
||||
|
||||
In most cases, this check will be used early in the view lifecycle and unfortunately,
|
||||
there's an Apple bug that causes multiple presentationControllers to be created if
|
||||
the presentationController is referenced here and called too early resulting in
|
||||
a strong reference to this view controller and in turn, creating a memory leak.
|
||||
*/
|
||||
public var isPanModalPresented: Bool {
|
||||
return (transitioningDelegate as? PanModalPresentationDelegate) != nil
|
||||
}
|
||||
|
||||
/**
|
||||
Configures a view controller for presentation using the PanModal transition
|
||||
|
||||
- Parameters:
|
||||
- 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,
|
||||
completion: (() -> Void)? = nil) {
|
||||
|
||||
/**
|
||||
Here, we deliberately do not check for size classes. More info in `PanModalPresentationDelegate`
|
||||
*/
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
viewControllerToPresent.modalPresentationStyle = .popover
|
||||
viewControllerToPresent.popoverPresentationController?.sourceRect = sourceRect
|
||||
viewControllerToPresent.popoverPresentationController?.sourceView = sourceView ?? view
|
||||
viewControllerToPresent.popoverPresentationController?.delegate = PanModalPresentationDelegate.default
|
||||
} else {
|
||||
viewControllerToPresent.modalPresentationStyle = .custom
|
||||
viewControllerToPresent.modalPresentationCapturesStatusBarAppearance = true
|
||||
viewControllerToPresent.transitioningDelegate = PanModalPresentationDelegate.default
|
||||
}
|
||||
|
||||
present(viewControllerToPresent, animated: true, completion: completion)
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
//
|
||||
// DimmedView.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2017 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import TISwiftUtils
|
||||
import TIUIKitCore
|
||||
import TIUIElements
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
A dim view for use as an overlay over content you want dimmed.
|
||||
*/
|
||||
public class DimmedView: BaseInitializableView {
|
||||
|
||||
public enum AppearanceType {
|
||||
|
||||
case transparent
|
||||
case transparentWithShadow(UIViewShadow)
|
||||
case opaque
|
||||
|
||||
var isTransparent: Bool {
|
||||
switch self {
|
||||
case .transparent, .transparentWithShadow:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Represents the possible states of the dimmed view.
|
||||
max, off or a percentage of dimAlpha.
|
||||
*/
|
||||
enum DimState {
|
||||
case max
|
||||
case off
|
||||
case percent(CGFloat)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/**
|
||||
The state of the dimmed view
|
||||
*/
|
||||
var dimState: DimState = .off {
|
||||
didSet {
|
||||
guard !appearanceType.isTransparent else {
|
||||
alpha = .zero
|
||||
return
|
||||
}
|
||||
|
||||
switch dimState {
|
||||
case .max:
|
||||
alpha = 1.0
|
||||
case .off:
|
||||
alpha = 0.0
|
||||
case .percent(let percentage):
|
||||
alpha = max(0.0, min(1.0, percentage))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
weak var presentingController: UIViewController?
|
||||
var appearanceType: AppearanceType
|
||||
|
||||
/**
|
||||
The closure to be executed when a tap occurs
|
||||
*/
|
||||
var didTap: VoidClosure?
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
init(presentingController: UIViewController? = nil,
|
||||
dimColor: UIColor = UIColor.black.withAlphaComponent(0.7),
|
||||
appearanceType: AppearanceType) {
|
||||
|
||||
self.presentingController = presentingController
|
||||
self.appearanceType = appearanceType
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
alpha = 0.0
|
||||
backgroundColor = dimColor
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
// MARK: - BaseInitializableView
|
||||
|
||||
public override func bindViews() {
|
||||
super.bindViews()
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapView))
|
||||
addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
// MARK: - Overrides
|
||||
|
||||
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if appearanceType.isTransparent {
|
||||
let subviews = presentingController?.view.subviews.reversed() ?? []
|
||||
|
||||
for subview in subviews {
|
||||
if let hittedView = subview.hitTest(point, with: event) {
|
||||
return hittedView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
!appearanceType.isTransparent
|
||||
}
|
||||
|
||||
// MARK: - Event Handlers
|
||||
|
||||
@objc private func didTapView() {
|
||||
didTap?()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
//
|
||||
// PanContainerView.swift
|
||||
// PanModal
|
||||
//
|
||||
// Copyright © 2018 Tiny Speck, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
A view wrapper around the presented view in a PanModal transition.
|
||||
|
||||
This allows us to make modifications to the presented view without
|
||||
having to do those changes directly on the view
|
||||
*/
|
||||
class PanContainerView: UIView {
|
||||
|
||||
init(presentedView: UIView, frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
addSubview(presentedView)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
|
||||
/**
|
||||
Convenience property for retrieving a PanContainerView instance
|
||||
from the view hierachy
|
||||
*/
|
||||
var panContainerView: PanContainerView? {
|
||||
return subviews.first(where: { view -> Bool in
|
||||
view is PanContainerView
|
||||
}) as? PanContainerView
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
ENV["DEVELOPMENT_INSTALL"] = "true"
|
||||
|
||||
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
|
||||
|
||||
target 'TIBottomSheet' do
|
||||
platform :ios, 11
|
||||
use_frameworks!
|
||||
|
|
|
|||
|
|
@ -82,19 +82,19 @@ class DetentsViewController: BaseModalViewController<UIView, UIView> {
|
|||
|
||||
В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране.
|
||||
|
||||
## DimmedView
|
||||
## DimmedView и PassthroughDimmedView
|
||||
|
||||
Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedViewType`. Это перечисление, содержащие следующие кейсы:
|
||||
|
||||
- opaque: dimmedView не прозрачен и чем выше будет подниматься, тем больше будет затемнение. В shortFormHeight прозрачность равна 0.0, в longFormHeight - 1.0.
|
||||
- transparent: dimmedView полностью прозрачен и будет пропускать все жесты на нижний (показывающий) контроллер
|
||||
- transparentWithShadow(_) dimmedView полностью прозрачен, однако модальное окно будет отбразывать тень на нижний контроллер. Все жесты так же проходят
|
||||
|
||||
> `UIViewShadow` получил статичное свойство для быстрой настройки тени котроллера
|
||||
Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedView`. Эти классы позволяют настраивать поведение при тапе в затемнённую область и кастомизировать затемнение под ваши нужды.
|
||||
*/
|
||||
class ShadowViewController: BaseModalViewController<UIView, UIView> {
|
||||
var dimmedViewType: DimmedView.AppearanceType {
|
||||
.transparentWithShadow(.defaultModalViewShadow)
|
||||
override var dimmedView: DimmedView? {
|
||||
let dimmedView = PassthroughDimmedView()
|
||||
dimmedView.hitTestHandlerView = view
|
||||
dimmedView.configureUIView(appearance: .init(shadow: UIViewShadow(radius: 8,
|
||||
color: .black,
|
||||
opacity: 0.3))
|
||||
|
||||
return dimmedView
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,13 +107,9 @@ class ShadowViewController: BaseModalViewController<UIView, UIView> {
|
|||
|
||||
Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола:
|
||||
|
||||
- `DragView` больше не добавляется самостоятельно автоматически. Однако теперь к нему есть доступ, так что его даже можно как угодно настроить
|
||||
- Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss`
|
||||
- Больше нет свойст отвечающих за разрешение закрыть модалку тапом или свайпом. Если какое-то из этих действий необходимо запретить, объявите соответствующее действие (`onTapToDismiss` и `onDragToDismiss`) с nil
|
||||
- Можно настроить промежуточное состояние модального окна с `mediumFormHeight`
|
||||
- `DimmedView` остается настраиваемым с помощью `dimmedViewType` свойства
|
||||
|
||||
Через протокол `PanModalPresentable` у вас остается доступ к `presentationDetents`. Однако его установка никак не повлияет на настройку высоты и положений модального окна.
|
||||
- `DimmedView` открыт для наследования и может создаваться в `dimmedView` у
|
||||
|
||||
> Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ Pod::Spec.new do |s|
|
|||
s.dependency 'TIUIElements', s.version.to_s
|
||||
s.dependency 'TIUIKitCore', s.version.to_s
|
||||
s.dependency 'TISwiftUtils', s.version.to_s
|
||||
s.dependency 'PanModal', '~> 1.3.0'
|
||||
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -86,20 +86,20 @@ class DetentsViewController: BaseModalViewController<UIView, UIView> {
|
|||
|
||||
В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране.
|
||||
|
||||
## DimmedView
|
||||
## DimmedView и PassthroughDimmedView
|
||||
|
||||
Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedViewType`. Это перечисление, содержащие следующие кейсы:
|
||||
|
||||
- opaque: dimmedView не прозрачен и чем выше будет подниматься, тем больше будет затемнение. В shortFormHeight прозрачность равна 0.0, в longFormHeight - 1.0.
|
||||
- transparent: dimmedView полностью прозрачен и будет пропускать все жесты на нижний (показывающий) контроллер
|
||||
- transparentWithShadow(_) dimmedView полностью прозрачен, однако модальное окно будет отбразывать тень на нижний контроллер. Все жесты так же проходят
|
||||
|
||||
> `UIViewShadow` получил статичное свойство для быстрой настройки тени котроллера
|
||||
Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedView`. Эти классы позволяют настраивать поведение при тапе в затемнённую область и кастомизировать затемнение под ваши нужды.
|
||||
|
||||
```swift
|
||||
class ShadowViewController: BaseModalViewController<UIView, UIView> {
|
||||
var dimmedViewType: DimmedView.AppearanceType {
|
||||
.transparentWithShadow(.defaultModalViewShadow)
|
||||
override var dimmedView: DimmedView? {
|
||||
let dimmedView = PassthroughDimmedView()
|
||||
dimmedView.hitTestHandlerView = view
|
||||
dimmedView.configureUIView(appearance: .init(shadow: UIViewShadow(radius: 8,
|
||||
color: .black,
|
||||
opacity: 0.3))
|
||||
|
||||
return dimmedView
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -112,12 +112,8 @@ class ShadowViewController: BaseModalViewController<UIView, UIView> {
|
|||
|
||||
Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола:
|
||||
|
||||
- `DragView` больше не добавляется самостоятельно автоматически. Однако теперь к нему есть доступ, так что его даже можно как угодно настроить
|
||||
- Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss`
|
||||
- Больше нет свойст отвечающих за разрешение закрыть модалку тапом или свайпом. Если какое-то из этих действий необходимо запретить, объявите соответствующее действие (`onTapToDismiss` и `onDragToDismiss`) с nil
|
||||
- Можно настроить промежуточное состояние модального окна с `mediumFormHeight`
|
||||
- `DimmedView` остается настраиваемым с помощью `dimmedViewType` свойства
|
||||
|
||||
Через протокол `PanModalPresentable` у вас остается доступ к `presentationDetents`. Однако его установка никак не повлияет на настройку высоты и положений модального окна.
|
||||
- `DimmedView` открыт для наследования и может создаваться в `dimmedView` у
|
||||
|
||||
> Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости.
|
||||
|
|
|
|||
Loading…
Reference in New Issue