Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save V8tr/bb4157dbab3f14b2f96cb8b51a44a6e2 to your computer and use it in GitHub Desktop.

Select an option

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.
//
// 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