Last active
July 27, 2022 02:33
-
-
Save ShaneQi/b6a035ea4620ce27bd0c89b004780901 to your computer and use it in GitHub Desktop.
This file contains 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
class MyViewController: UIViewController { | |
func showMenuFromBottom() { | |
let menuViewController = MyMenuViewController() | |
let cardViewController = InteractiveModalCardViewController() | |
cardViewController.install( | |
menuViewController, panGestureRecognizer: menuViewController.dismissGestureRecognizer) | |
present(cardViewController, animated: true, completion: nil) | |
} | |
} |
This file contains 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
// | |
// 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