Forked from vinczebalazs/SheetModalPresentationController.swift
Created
August 17, 2022 17:44
-
-
Save V8tr/bb4157dbab3f14b2f96cb8b51a44a6e2 to your computer and use it in GitHub Desktop.
A presentation controller to use for presenting a view controller modally, which can be dismissed by a pull down gesture. The presented view controller's height is also adjustable.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // SheetModalPresentationController.swift | |
| // | |
| // | |
| // Created by Balazs Vincze on 2020. 01. 14.. | |
| // Copyright © 2020. Balazs Vincze. All rights reserved. | |
| // | |
| import UIKit | |
| final class SheetModalPresentationController: UIPresentationController { | |
| // MARK: Private Properties | |
| private lazy var dimmingView: UIView = { | |
| let dimmingView = UIView() | |
| dimmingView.backgroundColor = .black | |
| return dimmingView | |
| }() | |
| private let interactor = UIPercentDrivenInteractiveTransition() | |
| private var isInteractive = false | |
| private let height: CGFloat | |
| private var gestureStart: Date! | |
| // MARK: Initializers | |
| init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, height: CGFloat) { | |
| self.height = min(height, presentedViewController.view.frame.height) | |
| super.init(presentedViewController: presentedViewController, presenting: presentingViewController) | |
| } | |
| // MARK: Function Overrides | |
| override var frameOfPresentedViewInContainerView: CGRect { | |
| guard let containerBounds = containerView?.bounds else { return .zero } | |
| var frame = containerBounds | |
| frame.size.height = height | |
| frame.origin.y = containerBounds.height - height | |
| return frame | |
| } | |
| override func presentationTransitionWillBegin() { | |
| super.presentationTransitionWillBegin() | |
| guard let containerBounds = containerView?.bounds else { return } | |
| presentedView?.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))) | |
| presentedView?.layer.cornerRadius = 40 | |
| dimmingView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismiss))) | |
| dimmingView.frame = containerBounds | |
| dimmingView.alpha = 0 | |
| containerView?.insertSubview(dimmingView, at: 0) | |
| presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in | |
| self.dimmingView.alpha = 0.5 | |
| }) | |
| } | |
| override func dismissalTransitionWillBegin() { | |
| super.dismissalTransitionWillBegin() | |
| presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in | |
| self.dimmingView.alpha = 0 | |
| }) | |
| } | |
| // MARK: Private Functions | |
| @objc private func dismiss() { | |
| presentedViewController.dismiss(animated: true) | |
| } | |
| @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { | |
| guard let gestureView = gesture.view, gesture.translation(in: gestureView).y >= 0 else { return } | |
| let percent = gesture.translation(in: gestureView).y / gestureView.bounds.height | |
| let velocityY = gesture.velocity(in: gestureView).y | |
| switch gesture.state { | |
| case .began: | |
| gestureStart = Date() | |
| isInteractive = true | |
| presentedViewController.dismiss(animated: true, completion: nil) | |
| case .changed: | |
| interactor.update(percent) | |
| case .cancelled: | |
| interactor.cancel() | |
| isInteractive = false | |
| case .ended: | |
| if percent > 0.5 || velocityY > 1300 { | |
| interactor.completionSpeed = max(1, (gestureView.frame.height * (1 / interactor.duration) / velocity.y)) | |
| interactor.completionSpeed = max(1, (gestureView.frame.height * (1 / interactor.duration) / velocityY)) | |
| interactor.finish() | |
| } else { | |
| interactor.cancel() | |
| } | |
| isInteractive = false | |
| default: | |
| break | |
| } | |
| } | |
| } | |
| // MARK: UIViewControllerTransitioningDelegate | |
| extension SheetPresentationController: UIViewControllerTransitioningDelegate { | |
| func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { | |
| self | |
| } | |
| func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
| SheetTransitionAnimator(.dismissed) | |
| } | |
| func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { | |
| isInteractive ? interactor : nil | |
| } | |
| } | |
| final class SheetTransitionAnimator: NSObject { | |
| enum TransitionType { | |
| case presented | |
| case dismissed | |
| } | |
| private let type: TransitionType | |
| private var propertyAnimator: UIViewPropertyAnimator! | |
| init(_ type: TransitionType) { | |
| self.type = type | |
| super.init() | |
| } | |
| } | |
| // MARK: UIViewControllerAnimatedTransitioning | |
| extension SheetTransitionAnimator: UIViewControllerAnimatedTransitioning { | |
| func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { | |
| 0.5 | |
| } | |
| func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { | |
| if type == .presented, let toView = transitionContext.view(forKey: .to) { | |
| transitionContext.containerView.addSubview(toView) | |
| toView.frame.origin.y = transitionContext.containerView.frame.maxY | |
| } | |
| interruptibleAnimator(using: transitionContext).startAnimation() | |
| } | |
| func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { | |
| propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), | |
| timingParameters: UICubicTimingParameters()) | |
| propertyAnimator.addAnimations { | |
| switch self.type { | |
| case .presented: | |
| guard let toVC = transitionContext.viewController(forKey: .to) else { break } | |
| toVC.view.frame = transitionContext.finalFrame(for: toVC) | |
| case .dismissed: | |
| transitionContext.view(forKey: .from)?.frame.origin.y = transitionContext.containerView.frame.maxY | |
| } | |
| } | |
| propertyAnimator.addCompletion { _ in | |
| transitionContext.completeTransition(!transitionContext.transitionWasCancelled) | |
| } | |
| return propertyAnimator | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment