Example of how to use presentation, animation & interaction controllers w/ custom segues to create a slide-in modal menu which partially covers presenting view.
import UIKit | |
enum Direction { | |
case Left, Right, Up, Down | |
var pointVector: CGPoint { | |
switch self { | |
case Left: return CGPoint(x: -1, y: 0) | |
case Right: return CGPoint(x: 1, y: 0) | |
case Up: return CGPoint(x: 0, y: -1) | |
case Down: return CGPoint(x: 0, y: 1) | |
} | |
} | |
} | |
class CoverPartiallyPresentationController: UIPresentationController { | |
var dismissInteractionController: PanGestureInteractionController? = nil | |
var interactiveDismissal: Bool = false | |
let coverDirection: Direction | |
private let margin: CGFloat = 64.0 | |
lazy private var backgroundView: UIView = { | |
let view = UIVisualEffectView(effect: UIBlurEffect(style: .Light)) | |
view.frame = self.containerView?.bounds ?? CGRectZero | |
view.backgroundColor = nil | |
let tapGesture = UITapGestureRecognizer(target: self, action: "backgroundViewTapped") | |
view.addGestureRecognizer(tapGesture) | |
return view | |
}() | |
init(presentedViewController: UIViewController, presentingViewController: UIViewController, coverDirection: Direction) { | |
self.coverDirection = coverDirection | |
super.init(presentedViewController: presentedViewController, presentingViewController: presentingViewController) | |
} | |
override func presentationTransitionWillBegin() { | |
containerView?.addSubview(backgroundView) | |
backgroundView.alpha = 0 | |
presentingViewController.transitionCoordinator()?.animateAlongsideTransition({ [weak self] _ in | |
self?.backgroundView.alpha = 1 | |
}, completion: nil) | |
} | |
override func dismissalTransitionWillBegin() { | |
presentingViewController.transitionCoordinator()?.animateAlongsideTransition({ [weak self] _ in | |
self?.backgroundView.alpha = 0 | |
}, completion: nil) | |
} | |
override func presentationTransitionDidEnd(completed: Bool) { | |
if !completed { | |
backgroundView.removeFromSuperview() | |
} | |
dismissInteractionController = PanGestureInteractionController(view: containerView!, direction: coverDirection) | |
dismissInteractionController?.callbacks.didBeginPanning = { [weak self] in | |
self?.interactiveDismissal = true | |
self?.presentingViewController.dismissViewControllerAnimated(true, completion: nil) | |
} | |
} | |
override func dismissalTransitionDidEnd(completed: Bool) { | |
interactiveDismissal = false | |
if completed { | |
backgroundView.removeFromSuperview() | |
} | |
} | |
override func frameOfPresentedViewInContainerView() -> CGRect { | |
guard let containerView = containerView else { | |
return CGRectZero | |
} | |
switch coverDirection { | |
case .Left: | |
return CGRect(x: 0, y: 0, width: containerView.bounds.width-margin, height: containerView.bounds.height) | |
case .Right: | |
return CGRect(x: margin, y: 0, width: containerView.bounds.width-margin, height: containerView.bounds.height) | |
case .Up: | |
return CGRect(x: 0, y: 0, width: containerView.bounds.width, height: containerView.bounds.height-margin) | |
case .Down: | |
return CGRect(x: 0, y: margin, width: containerView.bounds.width, height: containerView.bounds.height-margin) | |
} | |
} | |
func backgroundViewTapped() { | |
presentingViewController.dismissViewControllerAnimated(true, completion: nil) | |
} | |
} | |
class CoverPartiallySegue: UIStoryboardSegue, UIViewControllerTransitioningDelegate { | |
var direction: Direction = .Left | |
var presentationController: CoverPartiallyPresentationController! = nil | |
override func perform() { | |
destinationViewController.modalPresentationStyle = .Custom | |
destinationViewController.transitioningDelegate = self | |
super.perform() | |
} | |
func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController? { | |
presentationController = CoverPartiallyPresentationController(presentedViewController: presented, presentingViewController: presenting, coverDirection: direction) | |
return presentationController | |
} | |
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { | |
return presentationController.interactiveDismissal ? presentationController.dismissInteractionController : nil | |
} | |
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
return SlideInTransition(fromDirection: direction) | |
} | |
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
return SlideInTransition(fromDirection: direction, reverse: true, interactive: presentationController.interactiveDismissal) | |
} | |
} | |
class PanGestureInteractionController: UIPercentDrivenInteractiveTransition { | |
struct Callbacks { | |
var didBeginPanning: (() -> Void)? = nil | |
} | |
var callbacks = Callbacks() | |
let gestureRecognizer: UIPanGestureRecognizer | |
private let direction: Direction | |
// MARK: Initialization | |
init(view: UIView, direction: Direction) { | |
self.direction = direction | |
gestureRecognizer = UIPanGestureRecognizer() | |
view.addGestureRecognizer(gestureRecognizer) | |
super.init() | |
gestureRecognizer.delegate = self | |
gestureRecognizer.addTarget(self, action: "viewPanned:") | |
} | |
// MARK: User interaction | |
func viewPanned(sender: UIPanGestureRecognizer) { | |
switch sender.state { | |
case .Began: | |
callbacks.didBeginPanning?() | |
case .Changed: | |
updateInteractiveTransition(percentCompleteForTranslation(sender.translationInView(sender.view))) | |
case .Ended: | |
if sender.shouldRecognizeForDirection(direction) && percentComplete > 0.25 { | |
finishInteractiveTransition() | |
} else { | |
cancelInteractiveTransition() | |
} | |
case .Cancelled: | |
cancelInteractiveTransition() | |
default: | |
return | |
} | |
} | |
private func percentCompleteForTranslation(translation: CGPoint) -> CGFloat { | |
let panDistance = direction.panDistanceForView(gestureRecognizer.view!) | |
return (translation*panDistance)/(panDistance.magnitude*panDistance.magnitude) | |
} | |
} | |
extension PanGestureInteractionController: UIGestureRecognizerDelegate { | |
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool { | |
guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { | |
return false | |
} | |
return panGestureRecognizer.shouldRecognizeForDirection(direction) | |
} | |
} | |
private extension Direction { | |
func panDistanceForView(view: UIView) -> CGPoint { | |
switch self { | |
case .Left: return CGPoint(x: -view.bounds.size.width, y: 0) | |
case .Right: return CGPoint(x: view.bounds.size.width, y: 0) | |
case .Up: return CGPoint(x: 0, y: -view.bounds.size.height) | |
case .Down: return CGPoint(x: 0, y: view.bounds.size.height) | |
} | |
} | |
} | |
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning { | |
let duration: NSTimeInterval = 0.3 | |
let reverse: Bool | |
let interactive: Bool | |
let fromDirection: Direction | |
init(fromDirection: Direction, reverse: Bool = false, interactive: Bool = false) { | |
self.reverse = reverse | |
self.interactive = interactive | |
self.fromDirection = fromDirection | |
} | |
func animateTransition(transitionContext: UIViewControllerContextTransitioning) { | |
let viewControllerKey = reverse ? UITransitionContextFromViewControllerKey : UITransitionContextToViewControllerKey | |
let viewControllerToAnimate = transitionContext.viewControllerForKey(viewControllerKey)! | |
let viewToAnimate = viewControllerToAnimate.view | |
let offsetFrame = fromDirection.offsetFrameForView(viewToAnimate, containerView: transitionContext.containerView()!) | |
if !reverse { | |
transitionContext.containerView()?.addSubview(viewToAnimate) | |
viewToAnimate.frame = offsetFrame | |
} | |
let options: UIViewAnimationOptions = interactive ? [.CurveLinear] : [] | |
UIView.animateWithDuration(duration, delay: 0, options: options, | |
animations: { [weak self] in | |
if self!.reverse { | |
viewToAnimate.frame = offsetFrame | |
} else { | |
viewToAnimate.frame = transitionContext.finalFrameForViewController(viewControllerToAnimate) | |
} | |
}, completion: { _ in | |
transitionContext.completeTransition(!transitionContext.transitionWasCancelled()) | |
}) | |
} | |
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval { | |
return duration | |
} | |
} | |
private extension Direction { | |
func offsetFrameForView(view: UIView, containerView: UIView) -> CGRect { | |
var frame = view.bounds | |
switch self { | |
case .Left: | |
frame.origin.x = -frame.width | |
frame.origin.y = 0 | |
case .Right: | |
frame.origin.x = containerView.bounds.width | |
frame.origin.y = 0 | |
case .Up: | |
frame.origin.x = 0 | |
frame.origin.y = -frame.height | |
case .Down: | |
frame.origin.x = 0 | |
frame.origin.y = containerView.bounds.height | |
} | |
return frame | |
} | |
} | |
extension UIPanGestureRecognizer { | |
func shouldRecognizeForDirection(direction: Direction) -> Bool { | |
guard let view = view else { | |
return false | |
} | |
let velocity = velocityInView(view) | |
let a = angle(velocity, direction.pointVector) | |
return abs(a) < CGFloat(M_PI_4) // Angle should be within 45 degrees | |
} | |
} |
This comment has been minimized.
This comment has been minimized.
Improvised solution:
|
This comment has been minimized.
This comment has been minimized.
@MrAlek, thanks for your sample code. I cannot create a PR, but I have updated your code to work with Swift 4 here: https://gist.github.com/chrisco314/3b58040015ed857498c761a3ea524161 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
Thanks for this. Quick question: Is
angle
defined in a library? I'm not aware of any vector math functions being defined in Foundation or UIKit...