From 0ef1edfacb8ff8dcad0aef3c0f0c09e436b7368b Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Tue, 25 Jul 2023 17:32:21 +0300 Subject: [PATCH] build: move included pan modal sources to separate dependency --- Package.resolved | 9 + Package.swift | 5 +- TIBottomSheet/PlaygroundPodfile | 2 + .../BottomSheet/BaseModalViewController.swift | 1 + .../Models/ModalViewPresentationDetent.swift | 1 + .../Views/PassthroughDimmedView.swift | 51 + .../PanModal/Animator/PanModalAnimator.swift | 40 - .../PanModalPresentationAnimator.swift | 172 ---- .../PanModalPresentationController.swift | 881 ------------------ .../PanModalPresentationDelegate.swift | 81 -- TIBottomSheet/Sources/PanModal/LICENSE | 19 - TIBottomSheet/Sources/PanModal/PanModal.h | 19 - .../PanModal/Presentable/PanModalHeight.swift | 44 - .../PanModalPresentable+Defaults.swift | 145 --- .../PanModalPresentable+LayoutHelpers.swift | 126 --- ...PanModalPresentable+UIViewController.swift | 63 -- .../Presentable/PanModalPresentable.swift | 231 ----- .../Presenter/PanModalPresenter.swift | 38 - .../UIViewController+PanModalPresenter.swift | 66 -- .../Sources/PanModal/View/DimmedView.swift | 131 --- .../PanModal/View/PanContainerView.swift | 44 - .../TIBottomSheet.app/Contents/MacOS/Podfile | 2 + .../Contents.swift | 26 +- TIBottomSheet/TIBottomSheet.podspec | 1 + docs/tibottomsheet/tibottomsheet.md | 26 +- 25 files changed, 92 insertions(+), 2132 deletions(-) create mode 100644 TIBottomSheet/Sources/BottomSheet/Views/PassthroughDimmedView.swift delete mode 100644 TIBottomSheet/Sources/PanModal/Animator/PanModalAnimator.swift delete mode 100644 TIBottomSheet/Sources/PanModal/Animator/PanModalPresentationAnimator.swift delete mode 100644 TIBottomSheet/Sources/PanModal/Controller/PanModalPresentationController.swift delete mode 100644 TIBottomSheet/Sources/PanModal/Delegate/PanModalPresentationDelegate.swift delete mode 100644 TIBottomSheet/Sources/PanModal/LICENSE delete mode 100644 TIBottomSheet/Sources/PanModal/PanModal.h delete mode 100644 TIBottomSheet/Sources/PanModal/Presentable/PanModalHeight.swift delete mode 100644 TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+Defaults.swift delete mode 100644 TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift delete mode 100644 TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+UIViewController.swift delete mode 100644 TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable.swift delete mode 100644 TIBottomSheet/Sources/PanModal/Presenter/PanModalPresenter.swift delete mode 100644 TIBottomSheet/Sources/PanModal/Presenter/UIViewController+PanModalPresenter.swift delete mode 100644 TIBottomSheet/Sources/PanModal/View/DimmedView.swift delete mode 100644 TIBottomSheet/Sources/PanModal/View/PanContainerView.swift diff --git a/Package.resolved b/Package.resolved index 810d97dc..14b78560 100644 --- a/Package.resolved +++ b/Package.resolved @@ -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", diff --git a/Package.swift b/Package.swift index 1d157285..c7cfa56b 100644 --- a/Package.swift +++ b/Package.swift @@ -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")]), diff --git a/TIBottomSheet/PlaygroundPodfile b/TIBottomSheet/PlaygroundPodfile index e44c2e85..beb2a489 100644 --- a/TIBottomSheet/PlaygroundPodfile +++ b/TIBottomSheet/PlaygroundPodfile @@ -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! diff --git a/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift index 6114309b..01a7369e 100644 --- a/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift +++ b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift @@ -24,6 +24,7 @@ import TISwiftUtils import TIUIElements import TIUIKitCore import UIKit +import PanModal open class BaseModalViewController: BaseInitializableViewController, PanModalPresentable { diff --git a/TIBottomSheet/Sources/BottomSheet/Models/ModalViewPresentationDetent.swift b/TIBottomSheet/Sources/BottomSheet/Models/ModalViewPresentationDetent.swift index 5f0d568b..6948914a 100644 --- a/TIBottomSheet/Sources/BottomSheet/Models/ModalViewPresentationDetent.swift +++ b/TIBottomSheet/Sources/BottomSheet/Models/ModalViewPresentationDetent.swift @@ -21,6 +21,7 @@ // import Foundation +import PanModal public struct ModalViewPresentationDetent: Hashable { diff --git a/TIBottomSheet/Sources/BottomSheet/Views/PassthroughDimmedView.swift b/TIBottomSheet/Sources/BottomSheet/Views/PassthroughDimmedView.swift new file mode 100644 index 00000000..70056aed --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Views/PassthroughDimmedView.swift @@ -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) + } +} diff --git a/TIBottomSheet/Sources/PanModal/Animator/PanModalAnimator.swift b/TIBottomSheet/Sources/PanModal/Animator/PanModalAnimator.swift deleted file mode 100644 index 712dd5d2..00000000 --- a/TIBottomSheet/Sources/PanModal/Animator/PanModalAnimator.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/Animator/PanModalPresentationAnimator.swift b/TIBottomSheet/Sources/PanModal/Animator/PanModalPresentationAnimator.swift deleted file mode 100644 index f7fbdd0e..00000000 --- a/TIBottomSheet/Sources/PanModal/Animator/PanModalPresentationAnimator.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/Controller/PanModalPresentationController.swift b/TIBottomSheet/Sources/PanModal/Controller/PanModalPresentationController.swift deleted file mode 100644 index bf1dab90..00000000 --- a/TIBottomSheet/Sources/PanModal/Controller/PanModalPresentationController.swift +++ /dev/null @@ -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) { - - 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) { - - 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 diff --git a/TIBottomSheet/Sources/PanModal/Delegate/PanModalPresentationDelegate.swift b/TIBottomSheet/Sources/PanModal/Delegate/PanModalPresentationDelegate.swift deleted file mode 100644 index 24264b74..00000000 --- a/TIBottomSheet/Sources/PanModal/Delegate/PanModalPresentationDelegate.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/LICENSE b/TIBottomSheet/Sources/PanModal/LICENSE deleted file mode 100644 index 02bc46f1..00000000 --- a/TIBottomSheet/Sources/PanModal/LICENSE +++ /dev/null @@ -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. diff --git a/TIBottomSheet/Sources/PanModal/PanModal.h b/TIBottomSheet/Sources/PanModal/PanModal.h deleted file mode 100644 index 01451f52..00000000 --- a/TIBottomSheet/Sources/PanModal/PanModal.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// PanModal.h -// PanModal -// -// Created by Tosin A on 3/13/19. -// Copyright © 2019 Detail. All rights reserved. -// - -#import - -//! 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 - - diff --git a/TIBottomSheet/Sources/PanModal/Presentable/PanModalHeight.swift b/TIBottomSheet/Sources/PanModal/Presentable/PanModalHeight.swift deleted file mode 100644 index 00654f4e..00000000 --- a/TIBottomSheet/Sources/PanModal/Presentable/PanModalHeight.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+Defaults.swift b/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+Defaults.swift deleted file mode 100644 index d6c7dcd4..00000000 --- a/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+Defaults.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift b/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift deleted file mode 100644 index 0e93abe6..00000000 --- a/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+UIViewController.swift b/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+UIViewController.swift deleted file mode 100644 index 8de82fd5..00000000 --- a/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+UIViewController.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable.swift b/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable.swift deleted file mode 100644 index aa008aed..00000000 --- a/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/Presenter/PanModalPresenter.swift b/TIBottomSheet/Sources/PanModal/Presenter/PanModalPresenter.swift deleted file mode 100644 index 8763fdac..00000000 --- a/TIBottomSheet/Sources/PanModal/Presenter/PanModalPresenter.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/Presenter/UIViewController+PanModalPresenter.swift b/TIBottomSheet/Sources/PanModal/Presenter/UIViewController+PanModalPresenter.swift deleted file mode 100644 index b252d8f7..00000000 --- a/TIBottomSheet/Sources/PanModal/Presenter/UIViewController+PanModalPresenter.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/View/DimmedView.swift b/TIBottomSheet/Sources/PanModal/View/DimmedView.swift deleted file mode 100644 index 8ca838d2..00000000 --- a/TIBottomSheet/Sources/PanModal/View/DimmedView.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/Sources/PanModal/View/PanContainerView.swift b/TIBottomSheet/Sources/PanModal/View/PanContainerView.swift deleted file mode 100644 index f5c2892b..00000000 --- a/TIBottomSheet/Sources/PanModal/View/PanContainerView.swift +++ /dev/null @@ -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 diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/Podfile b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/Podfile index e44c2e85..beb2a489 100644 --- a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/Podfile +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/Podfile @@ -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! diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Pages/TIBottomSheet.xcplaygroundpage/Contents.swift b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Pages/TIBottomSheet.xcplaygroundpage/Contents.swift index 05a69f72..3892076c 100644 --- a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Pages/TIBottomSheet.xcplaygroundpage/Contents.swift +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Pages/TIBottomSheet.xcplaygroundpage/Contents.swift @@ -82,19 +82,19 @@ class DetentsViewController: BaseModalViewController { В данный массив не рекомендуется передавать больше 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 { - 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 { Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола: - - `DragView` больше не добавляется самостоятельно автоматически. Однако теперь к нему есть доступ, так что его даже можно как угодно настроить - Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss` - - Больше нет свойст отвечающих за разрешение закрыть модалку тапом или свайпом. Если какое-то из этих действий необходимо запретить, объявите соответствующее действие (`onTapToDismiss` и `onDragToDismiss`) с nil - Можно настроить промежуточное состояние модального окна с `mediumFormHeight` - - `DimmedView` остается настраиваемым с помощью `dimmedViewType` свойства - - Через протокол `PanModalPresentable` у вас остается доступ к `presentationDetents`. Однако его установка никак не повлияет на настройку высоты и положений модального окна. + - `DimmedView` открыт для наследования и может создаваться в `dimmedView` у > Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости. */ diff --git a/TIBottomSheet/TIBottomSheet.podspec b/TIBottomSheet/TIBottomSheet.podspec index 3659dafc..8c100fd9 100644 --- a/TIBottomSheet/TIBottomSheet.podspec +++ b/TIBottomSheet/TIBottomSheet.podspec @@ -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 diff --git a/docs/tibottomsheet/tibottomsheet.md b/docs/tibottomsheet/tibottomsheet.md index 5086aa85..251cf60e 100644 --- a/docs/tibottomsheet/tibottomsheet.md +++ b/docs/tibottomsheet/tibottomsheet.md @@ -86,20 +86,20 @@ class DetentsViewController: BaseModalViewController { В данный массив не рекомендуется передавать больше 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 { - 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 { Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола: - - `DragView` больше не добавляется самостоятельно автоматически. Однако теперь к нему есть доступ, так что его даже можно как угодно настроить - Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss` - - Больше нет свойст отвечающих за разрешение закрыть модалку тапом или свайпом. Если какое-то из этих действий необходимо запретить, объявите соответствующее действие (`onTapToDismiss` и `onDragToDismiss`) с nil - Можно настроить промежуточное состояние модального окна с `mediumFormHeight` - - `DimmedView` остается настраиваемым с помощью `dimmedViewType` свойства - - Через протокол `PanModalPresentable` у вас остается доступ к `presentationDetents`. Однако его установка никак не повлияет на настройку высоты и положений модального окна. + - `DimmedView` открыт для наследования и может создаваться в `dimmedView` у > Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости.