Created
December 6, 2015 19:09
-
-
Save MrAlek/3d1520ca2c5d981489e2 to your computer and use it in GitHub Desktop.
Example of how to use presentation, animation & interaction controllers w/ custom segues to create a slide-in modal menu which partially covers presenting view.
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
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 | |
} | |
} |
Improvised solution:
func angle(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
// TODO | - Not sure if this is correct
return atan2(a.y, a.x) - atan2(b.y, b.x)
}
@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
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...