Skip to content

Instantly share code, notes, and snippets.

@ShaneQi
Last active July 27, 2022 02:33
Show Gist options
  • Save ShaneQi/b6a035ea4620ce27bd0c89b004780901 to your computer and use it in GitHub Desktop.
Save ShaneQi/b6a035ea4620ce27bd0c89b004780901 to your computer and use it in GitHub Desktop.
class MyViewController: UIViewController {
func showMenuFromBottom() {
let menuViewController = MyMenuViewController()
let cardViewController = InteractiveModalCardViewController()
cardViewController.install(
menuViewController, panGestureRecognizer: menuViewController.dismissGestureRecognizer)
present(cardViewController, animated: true, completion: nil)
}
}
//
// InteractiveModalCardViewController.swift
// Eastwatch
//
// Created by Shane Qi on 1/14/19.
// Copyright © 2019 Shane Qi. All rights reserved.
//
import UIKit
final class InteractiveModalCardViewController: UIViewController {
private let visualEffectView: UIVisualEffectView = {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
view.alpha = 0
view.translatesAutoresizingMaskIntoConstraints = false
return view
} ()
private let cardContainer: UIView = {
let view = UIView()
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.layer.cornerRadius = 16
view.clipsToBounds = true
view.translatesAutoresizingMaskIntoConstraints = false
return view
} ()
private let backgroundTappingRecognizer = UITapGestureRecognizer()
private var hiddingCardConstraint: NSLayoutConstraint?
private var showingCardConstraint: NSLayoutConstraint?
private var animator: UIViewPropertyAnimator?
private var isDismissingInteractively: Bool?
private var transitionContext: UIViewControllerContextTransitioning?
private var dismissGestureTotalDistance: CGFloat?
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
transitioningDelegate = self
modalPresentationStyle = .overFullScreen
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(backgroundTappingRecognizer)
backgroundTappingRecognizer.addTarget(self, action: #selector(didTapBackground(_:)))
view.addSubview(visualEffectView)
NSLayoutConstraint.activate([
visualEffectView.leftAnchor.constraint(equalTo: view.leftAnchor),
visualEffectView.rightAnchor.constraint(equalTo: view.rightAnchor),
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
view.addSubview(cardContainer)
showingCardConstraint = cardContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor)
hiddingCardConstraint = cardContainer.topAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.activate([
cardContainer.leftAnchor.constraint(equalTo: view.leftAnchor),
cardContainer.rightAnchor.constraint(equalTo: view.rightAnchor),
hiddingCardConstraint
].compactMap({ $0 }))
}
func install(_ viewController: UIViewController, panGestureRecognizer: UIPanGestureRecognizer) {
addChild(viewController)
cardContainer.addSubview(viewController.view)
viewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
viewController.view.leftAnchor.constraint(equalTo: cardContainer.leftAnchor),
viewController.view.rightAnchor.constraint(equalTo: cardContainer.rightAnchor),
viewController.view.topAnchor.constraint(equalTo: cardContainer.topAnchor),
viewController.view.bottomAnchor.constraint(equalTo: cardContainer.bottomAnchor)
])
viewController.didMove(toParent: self)
panGestureRecognizer.addTarget(self, action: #selector(didChange(dismissGestureRecognizer:)))
}
@objc private func didTapBackground(_ gestureRecognizer: UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: cardContainer.superview)
guard !cardContainer.frame.contains(location) else {
return
}
isDismissingInteractively = false
dismiss(animated: true, completion: nil)
}
@objc private func didChange(dismissGestureRecognizer recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
dismissGestureTotalDistance = cardContainer.frame.height
isDismissingInteractively = true
dismiss(animated: true, completion: nil)
recognizer.setTranslation(.zero, in: view)
case .changed:
guard let animator = animator, let context = transitionContext,
let totalDistance = dismissGestureTotalDistance else { break }
let deltaY = recognizer.translation(in: view).y
var deltaFractionCompletion = deltaY / totalDistance
if deltaFractionCompletion > 1 {
deltaFractionCompletion = 1
} else if deltaFractionCompletion < 0 {
deltaFractionCompletion = 0
}
animator.fractionComplete = deltaFractionCompletion
context.updateInteractiveTransition(deltaFractionCompletion)
case .cancelled, .ended:
guard let animator = animator else { break }
if animator.fractionComplete < 0.5, recognizer.velocity(in: view).y < 500 {
transitionContext?.cancelInteractiveTransition()
animator.isReversed = true
} else {
transitionContext?.finishInteractiveTransition()
}
animator.continueAnimation(
withTimingParameters: UISpringTimingParameters(dampingRatio: 1), durationFactor: 0.5)
default: break
}
}
}
extension InteractiveModalCardViewController: UIViewControllerTransitioningDelegate {
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func interactionControllerForDismissal(
using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if isDismissingInteractively == true {
return self
} else {
return nil
}
}
}
extension InteractiveModalCardViewController: UIViewControllerAnimatedTransitioning {
var transitionDuration: TimeInterval {
return 0.25
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return transitionDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if let toView = transitionContext.view(forKey: .to), toView === view {
view.layoutIfNeeded()
transitionContext.containerView.addSubview(toView)
UIView.animate(withDuration: transitionDuration, animations: {
self.visualEffectView.alpha = 1
self.hiddingCardConstraint?.isActive = false
self.showingCardConstraint?.isActive = true
self.view.layoutIfNeeded()
}, completion: { _ in
transitionContext.completeTransition(true)
})
} else {
UIView.animate(withDuration: transitionDuration, animations: {
self.visualEffectView.alpha = 0
self.cardContainer.alpha = 0
self.showingCardConstraint?.isActive = false
self.hiddingCardConstraint?.isActive = true
self.view.layoutIfNeeded()
}, completion: { _ in
self.isDismissingInteractively = nil
transitionContext.completeTransition(true)
})
}
}
}
extension InteractiveModalCardViewController: UIViewControllerInteractiveTransitioning {
func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
animator = UIViewPropertyAnimator(duration: transitionDuration, curve: .linear) {
self.visualEffectView.alpha = 0
self.cardContainer.alpha = 0
self.showingCardConstraint?.isActive = false
self.hiddingCardConstraint?.isActive = true
self.view.layoutIfNeeded()
}
animator?.addCompletion { position in
self.isDismissingInteractively = nil
switch position {
case .end:
transitionContext.completeTransition(true)
default:
self.cardContainer.alpha = 1
self.visualEffectView.alpha = 1
self.hiddingCardConstraint?.isActive = false
self.showingCardConstraint?.isActive = true
transitionContext.completeTransition(false)
}
}
animator?.pauseAnimation()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment