Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
}
}
@SwiftsNamesake

This comment has been minimized.

Copy link

@SwiftsNamesake SwiftsNamesake commented Dec 29, 2017

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

@SwiftsNamesake

This comment has been minimized.

Copy link

@SwiftsNamesake SwiftsNamesake commented Dec 29, 2017

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)
}
@chrislconover

This comment has been minimized.

Copy link

@chrislconover chrislconover commented Jan 23, 2018

@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