21

By default, if you drag right from the left edge of the screen, it will drag away the ViewController and take it off the stack.

I want to extend this functionality to the entire screen. When the user drags right anywhere, I'd like the same to happen.

I know that I can implement a swipe right gesture and simply call self.navigationController?.popViewControllerAnimated(true)

However, there is no "dragging" motion. I want the user to be able to right-drag the view controller as if it's an object, revealing what's underneath. And, if it's dragged past 50%, dismiss it. (Check out instagram to see what I mean.)

TIMEX
  • 238,746
  • 336
  • 750
  • 1,061
  • I think this would be helpful for you https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIScreenEdgePanGestureRecognizer_class/index.html#//apple_ref/occ/cl/UIScreenEdgePanGestureRecognizer – ColdSteel Feb 14 '16 at 07:05
  • Does Instagram dismiss it past 33%? I just tried, but it seems to me at 50% to dismiss the view. – Blaszard Feb 18 '16 at 20:05
  • @Blaszard just updated . – TIMEX Feb 19 '16 at 07:01
  • If there was no bounty, I would close this question as too broad. There are multiple tutorials (Apple, blogs) and multiple github libraries. – Sulthan Feb 19 '16 at 17:12
  • These libraries do exactly what you are asking for: [fastred/SloppySwiper](https://github.com/fastred/SloppySwiper) and [jaredsinclair/JTSSloppySwiping](https://github.com/jaredsinclair/JTSSloppySwiping) – ElectroBuddha Jul 08 '17 at 10:19
  • Added a much simpler solution see my answer below. – Kugutsumen Sep 03 '19 at 04:35

7 Answers7

33

enter image description here

Made a demo project in Github
https://github.com/rishi420/SwipeRightToPopController

I've used UIViewControllerAnimatedTransitioning protocol

From the doc:

// This is used for percent driven interactive transitions, as well as for container controllers ...

Added a UIPanGestureRecognizer to the controller's view. This is the action of the gesture:

func handlePanGesture(panGesture: UIPanGestureRecognizer) {

    let percent = max(panGesture.translationInView(view).x, 0) / view.frame.width

    switch panGesture.state {

    case .Began:
        navigationController?.delegate = self
        navigationController?.popViewControllerAnimated(true)

    case .Changed:
        percentDrivenInteractiveTransition.updateInteractiveTransition(percent)

    case .Ended:
        let velocity = panGesture.velocityInView(view).x

        // Continue if drag more than 50% of screen width or velocity is higher than 1000
        if percent > 0.5 || velocity > 1000 {
            percentDrivenInteractiveTransition.finishInteractiveTransition()
        } else {
            percentDrivenInteractiveTransition.cancelInteractiveTransition()
        }

    case .Cancelled, .Failed:
        percentDrivenInteractiveTransition.cancelInteractiveTransition()

    default:
        break
    }
}

Steps:

  1. Calculate the percentage of drag on the view
  2. .Begin: Specify which segue to perform and assign UINavigationController delegate. delegate will be needed for InteractiveTransitioning
  3. .Changed: UpdateInteractiveTransition with percentage
  4. .Ended: Continue remaining transitioning if drag 50% or more or higher velocity else cancel
  5. .Cancelled, .Failed: cancel transitioning


References:

  1. UIPercentDrivenInteractiveTransition
  2. https://github.com/visnup/swipe-left
  3. https://github.com/robertmryan/ScreenEdgeGestureNavigationController
  4. https://github.com/groomsy/custom-navigation-animation-transition-demo
Warif Akhand Rishi
  • 23,094
  • 6
  • 79
  • 103
  • Is there any way to do this without having to recreate the default pop transition, that you know of? – Tim Vermeulen Feb 23 '16 at 11:06
  • The step 2 `case .Begin:` where you specify the `segue`. it could be any segue of your choice. – Warif Akhand Rishi Feb 23 '16 at 11:47
  • That's not what I meant. In your sample project, you make a `SlideAnimatedTransitioning` class that mimics the default transition animation, right? I was wondering if I can simply use the default animation, rather than having to mimic it... – Tim Vermeulen Feb 23 '16 at 11:53
  • How can i use this example to SwipeLeftToPushController ? – Mridul Gupta Apr 24 '17 at 10:23
  • This is overkill... you can just copy the action over to a pan gesture recogniser. See my answer below. – Kugutsumen Sep 03 '19 at 04:35
  • Hello @WarifAkhandRishi, is there a way i can implement this to dismiss a UIViewController that was presented rather than pushed? – Israel Meshileya Nov 14 '19 at 08:01
  • @IsraelMeshileya by default iOS 13 does this on presented VC. You can check out [PanSlip](https://github.com/k-lpmg/PanSlip) or similar open source repo. – Warif Akhand Rishi Nov 23 '19 at 02:35
9

Create a pan gesture recogniser and move the interactive pop gesture recogniser's targets across.

Add your recogniser to the pushed view controller's viewDidLoad and voila!

Edit: Updated the code with more detailed solution.

import os
import UIKit

public extension UINavigationController {
  func fixInteractivePopGestureRecognizer(delegate: UIGestureRecognizerDelegate) {
    guard
      let popGestureRecognizer = interactivePopGestureRecognizer,
      let targets = popGestureRecognizer.value(forKey: "targets") as? NSMutableArray,
      let gestureRecognizers = view.gestureRecognizers,
      // swiftlint:disable empty_count
      targets.count > 0
    else { return }

    if viewControllers.count == 1 {
      for recognizer in gestureRecognizers where recognizer is PanDirectionGestureRecognizer {
        view.removeGestureRecognizer(recognizer)
        popGestureRecognizer.isEnabled = false
        recognizer.delegate = nil
      }
    } else {
      if gestureRecognizers.count == 1 {
        let gestureRecognizer = PanDirectionGestureRecognizer(axis: .horizontal, direction: .right)
        gestureRecognizer.cancelsTouchesInView = false
        gestureRecognizer.setValue(targets, forKey: "targets")
        gestureRecognizer.require(toFail: popGestureRecognizer)
        gestureRecognizer.delegate = delegate
        popGestureRecognizer.isEnabled = true

        view.addGestureRecognizer(gestureRecognizer)
      }
    }
  }
}

public enum PanAxis {
  case vertical
  case horizontal
}

public enum PanDirection {
  case left
  case right
  case up
  case down
  case normal
}

public class PanDirectionGestureRecognizer: UIPanGestureRecognizer {
  let axis: PanAxis
  let direction: PanDirection

  public init(axis: PanAxis, direction: PanDirection = .normal, target: AnyObject? = nil, action: Selector? = nil) {
    self.axis = axis
    self.direction = direction
    super.init(target: target, action: action)
  }

  override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
    super.touchesMoved(touches, with: event)

    if state == .began {
      let vel = velocity(in: view)
      switch axis {
      case .horizontal where abs(vel.y) > abs(vel.x):
        state = .cancelled
      case .vertical where abs(vel.x) > abs(vel.y):
        state = .cancelled
      default:
        break
      }

      let isIncrement = axis == .horizontal ? vel.x > 0 : vel.y > 0

      switch direction {
      case .left where isIncrement:
        state = .cancelled
      case .right where !isIncrement:
        state = .cancelled
      case .up where isIncrement:
        state = .cancelled
      case .down where !isIncrement:
        state = .cancelled
      default:
        break
      }
    }
  }
}

In your collection view for example:

  open override func didMove(toParent parent: UIViewController?) {
    navigationController?.fixInteractivePopGestureRecognizer(delegate: self)
  }

// MARK: - UIGestureRecognizerDelegate
extension BaseCollection: UIGestureRecognizerDelegate {
  public func gestureRecognizer(
    _ gestureRecognizer: UIGestureRecognizer,
    shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer
  ) -> Bool {
    otherGestureRecognizer is PanDirectionGestureRecognizer
  }
}
Kugutsumen
  • 790
  • 7
  • 15
2

Swift 4 version of the accepted answer by @Warif Akhand Rishi

Even though this answer does work there are 2 quirks that I found out about it.

  1. if you swipe left it also dismisses just as if you were swiping right.
  2. it's also very delicate because if even a slight swipe is directed in either direction it will dismiss the vc.

Other then that it definitely works and you can swipe either right or left to dismiss.

class ViewController: UIGestureRecognizerDelegate, UINavigationControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationController?.interactivePopGestureRecognizer?.delegate = self
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        view.addGestureRecognizer(panGesture)
    }

    @objc func handlePanGesture(_ gesture: UIPanGestureRecognizer){

        let interactiveTransition = UIPercentDrivenInteractiveTransition()

        let percent = max(gesture.translation(in: view).x, 0) / view.frame.width

        switch gesture.state {

        case .began:
            navigationController?.delegate = self

            // *** use this if the vc is PUSHED on the stack **
            navigationController?.popViewController(animated: true)

            // *** use this if the vc is PRESENTED **
            //navigationController?.dismiss(animated: true, completion: nil)

        case .changed:
            interactiveTransition.update(percent)

        case .ended:
            let velocity = gesture.velocity(in: view).x

            // Continue if drag more than 50% of screen width or velocity is higher than 1000
            if percent > 0.5 || velocity > 1000 {
                interactiveTransition.finish()
            } else {
                interactiveTransition.cancel()
            }

        case .cancelled, .failed:
            interactiveTransition.cancel()

        default:break
        }
    }
}
Lance Samaria
  • 14,568
  • 10
  • 83
  • 201
  • You solution is poping viewcontroller very fast – Dhruv Narayan Singh Aug 29 '18 at 10:14
  • Hello @LanceSamaria is there a way i can implement this to dismiss a UIViewController that was presented rather than pushed? – Israel Meshileya Nov 14 '19 at 08:03
  • @IsraelMeshileya I haven’t tried it but maybe change the line that says **navigationController?.popViewController(animated(: true)** to **dismiss...** – Lance Samaria Nov 14 '19 at 08:18
  • oh, not at all. I am still trying out some other tweaks but will let you know once I am through. – Israel Meshileya Nov 15 '19 at 07:38
  • @IsraelMeshileya you must be doing something wrong. I just tried the code myself and it works fine. I present the vc like this let myVC = MyVC(); let navVC = UINav...(root...: myVC); present(navVC....). I add the above code from my answer in MyVC and when I swipe left or right it dismisses. In **case: .began** you have to swap this line: **navigationController?.popViewController(animated(: true)** to use this line instead: **navigationController?.dismiss(animated: true, completion: nil)**. Look at the code, I added it in there for you. I updated the **case: .began** with it – Lance Samaria Nov 15 '19 at 10:20
  • 1
    @IsraelMeshileya I added the project to GitHub. Just download it, run it, tap the button and after myVC is presented, swipe to dismiss and it works fine. There is a cancelButton in the upper left hand corner, you do not need to tap it to cancel. Here: https://github.com/lsamaria/SwipeToDismiss .Please let me know if it works for you – Lance Samaria Nov 15 '19 at 10:47
0

You need to investigate the interactivePopGestureRecognizer property of your UINavigationController.

Here is a similar question with example code to hook this up.

UINavigationController interactivePopGestureRecognizer working abnormal in iOS7

Community
  • 1
  • 1
TomSwift
  • 39,189
  • 11
  • 118
  • 147
-2

Swipe Right to dismiss the View Controller

Swift 5 Version - (Also removed the gesture recognition when swiping from right - to - left)

Important - In ‘Attributes inspector’ of VC2, set the ‘Presentation’ value from ‘Full Screen’ to ‘Over Full Screen’. This will allow VC1 to be visible during dismissing VC2 via gesture — without it, there will be black screen behind VC2 instead of VC1.

class ViewController: UIGestureRecognizerDelegate, UINavigationControllerDelegate {
    
    var initialTouchPoint: CGPoint = CGPoint(x: 0, y: 0)
    
    override func viewDidLoad() {
        super.viewDidLoad()

        navigationController?.interactivePopGestureRecognizer?.delegate = self
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        view.addGestureRecognizer(panGesture)
    }

    @objc func handlePanGesture(_ sender: UIPanGestureRecognizer) {
        let touchPoint = sender.location(in: self.view?.window)
        let percent = max(sender.translation(in: view).x, 0) / view.frame.width
        let velocity = sender.velocity(in: view).x
        
        if sender.state == UIGestureRecognizer.State.began {
            initialTouchPoint = touchPoint
        } else if sender.state == UIGestureRecognizer.State.changed {
            if touchPoint.x - initialTouchPoint.x > 0 {
                self.view.frame = CGRect(x: touchPoint.x - initialTouchPoint.x, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
            }
        } else if sender.state == UIGestureRecognizer.State.ended || sender.state == UIGestureRecognizer.State.cancelled {
            
            if percent > 0.5 || velocity > 1000 {
                navigationController?.popViewController(animated: true)
            } else {
                UIView.animate(withDuration: 0.3, animations: {
                    self.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
                })
            }
        }
    }
}
Nimantha
  • 5,793
  • 5
  • 23
  • 56
Fury2K
  • 1
-2

I think this is easier than the suggested solution and also works for all viewControllers inside that navigation and also for nested scrollviews.

https://stackoverflow.com/a/58779146/8517882

Just install the pod and then use EZNavigationController instead of UINavigationController to have this behavior on all view controllers inside that navigation controller.

Enricoza
  • 992
  • 5
  • 17
-2

Answers are too complicated. There is a simple solution. Add next line to your base navigation controller, or navigation controller that you want to have this ability:

  self.interactivePopGestureRecognizer?.delegate = nil
Nimantha
  • 5,793
  • 5
  • 23
  • 56