Swift : Pull down to dismiss `UITableViewController` - ios

I want to pull down to dismiss UITableViewController so I used scrollViewDidScroll method but it didn't works!
class CommentViewController: PFQueryTableViewController {
private let tableHeaderHeight: CGFloat = 350.0
extension CommentViewController
{
override func scrollViewDidScroll(scrollView: UIScrollView)
{
// Pull down to dismiss TVC
let offsetY = scrollView.contentOffset.y
let adjustment: CGFloat = 130.0
// for later use
if (-offsetY) > (tableHeaderHeight+adjustment) {
self.dismissViewControllerAnimated(true, completion: nil)
}
}
}

Swift 4
var panGestureRecognizer : UIPanGestureRecognizer!
override func viewDidLoad() {
mainTableView.bounces = true
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panRecognized))
panGestureRecognizer.delegate = self
mainTableView.addGestureRecognizer(panGestureRecognizer)
}
#objc func panRecognized(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began && mainTableView.contentOffset.y == 0 {
} else if recognizer.state != .ended && recognizer.state != .cancelled && recognizer.state != .failed {
let panOffset = recognizer.translation(in: mainTableView)
let eligiblePanOffset = panOffset.y > 300
if eligiblePanOffset {
recognizer.isEnabled = false
recognizer.isEnabled = true
self.dismiss(animated: true, completion: nil)
}
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

You have to implement additional pan gesture recognizer which will recognize simultaneously with scrollView's pan gesture recognizer. Then you can determine whether user is panning by his finger when table view is already scrolled to the top.
e.g.
var isTrackingPanLocation = false
var panGestureRecognizer: UIPanGestureRecognizer!
public override func viewDidLoad() {
super.viewDidLoad()
tableView.bounces = false
panGestureRecognizer = UIPanGestureRecognizer(target: self,
action: #selector(panRecognized(gestureRecognizer:)))
panGestureRecognizer.delegate = self
tableView.addGestureRecognizer(panGestureRecognizer)
}
public func panRecognized(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began && tableView.contentOffset.y == 0 {
recognizer.setTranslation(CGPoint.zero, inView : tableView)
isTrackingPanLocation = true
} else if recognizer.state != .ended &&
recognizer.state != .cancelled &&
recognizer.state != .failed &&
isTrackingPanLocation {
let panOffset = recognizer.translationInView(tableView)
// determine offset of the pan from the start here.
// When offset is far enough from table view top edge -
// dismiss your view controller. Additionally you can
// determine if pan goes in the wrong direction and
// then reset flag isTrackingPanLocation to false
let eligiblePanOffset = panOffset.y > 200
if eligiblePanOffset {
recognizer.enabled = false
recognizer.enabled = true
dismissViewControllerAnimated(true, completion: nil)
}
if panOffset.y < 0 {
isTrackingPanLocation = false
}
} else {
isTrackingPanLocation = false
}
}
public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWithGestureRecognizer
otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

Why don't you place print(offsetY) in scrollViewDidScroll. I suspect that (-offsetY) > (tableHeaderHeight+adjustment) will never be satisfied because of the rubber banding will cause the tableview to rebound before it can dismiss the view controller

For people looking at this in 2019 -- A more modern approach would use the UIGestureRecognizerDelegate methods, instead of keeping extra state in your view controller. For example:
private weak var panFromTop: UIPanGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
// Add pan gesture recognizer
let panFromTop = UIPanGestureRecognizer(target: self, action: #selector(handlePanFromTop(_:)))
panFromTop.delegate = self
tableView.addGestureRecognizer(panFromTop)
self.panFromTop = panFromTop
}
#objc func handlePanFromTop(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
// TODO: BEGIN YOUR ANIMATION HERE
case .changed:
// TODO: UPDATE YOUR ANIMATION HERE
default:
let translation = recognizer.translation(in: view)
let velocity = recognizer.velocity(in: view)
if ((translation.y + velocity.y) / view.bounds.height) > 0.5 {
// TODO: FINISH YOUR ANIMATION HERE
} else {
// TODO: CANCEL YOUR ANIMATION HERE
}
}
}
Disable bounce at the top of the table view only:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
scrollView.setContentOffset(.zero, animated: false)
}
}
Then implement the gesture recognizer delegate methods:
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
return true
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let recognizer = gestureRecognizer as? UIPanGestureRecognizer,
recognizer === panFromTop else {
// Only require special conditions for the panFromTop gesture recognizer
return true
}
// Require the scroll view to be at the top,
// and require the pan to start by dragging downward
return (
tableView.contentOffset.y <= 0 &&
recognizer.velocity(in: view).y > 0
)
}

Related

How to detect that multiple UIGestureRecognizers end?

in my view I have multiple gesture recognizers (UIPanGestureRecognizer, UIPinchGestureRecognizer and UIRotationGestureRecognizer) and I allowed them to detect touches simultaneously:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.view != otherGestureRecognizer.view {
return false
}
return true
}
The problem with this approach is that I am no longer able to detect when all the 3 gestures end. I cannot use
if gesture.state == .ended || gesture.state == .cancelled {
}
bacause this is valid only for a gesture recognizer and not for the all 3.
Any idea if there an api to detect when all active recognizers end?
Thanks
Solution
This works but it is very ugly: basically I keep track when all three gestures recognizers end and prevent to detect the end multiple times as the recognizers callback can be called in any order:
class SCCanvasViewController: UIViewController {
var gesturesAlreadyEnded = false
lazy var panGestureRecognizer: UIPanGestureRecognizer = {
let gr = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
gr.delegate = self
return gr
}()
lazy var pinchGestureRecognizer: UIPinchGestureRecognizer = {
let gr = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
gr.delegate = self
return gr
}()
lazy var rotateGestureRecognizer: UIRotationGestureRecognizer = {
let gr = UIRotationGestureRecognizer(target: self, action: #selector(handleRotation(_:)))
gr.delegate = self
return gr
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(panGestureRecognizer)
view.addGestureRecognizer(pinchGestureRecognizer)
view.addGestureRecognizer(rotateGestureRecognizer)
}
func isGestureEnded(gesture: UIGestureRecognizer) -> Bool {
return gesture.state == .ended || gesture.state == .cancelled || gesture.state == .failed
}
func allGesturesEnded() -> Bool {
let panEnded = isGestureEnded(gesture: panGestureRecognizer)
let pinchEnded = isGestureEnded(gesture: pinchGestureRecognizer)
let rotationEnded = isGestureEnded(gesture: rotateGestureRecognizer)
return panEnded && pinchEnded && rotationEnded
}
#objc func handlePan(_ gesture: UIPanGestureRecognizer) {
if gesture.state == .began {
gesturesAlreadyEnded = false
}
if !gesturesAlreadyEnded && isGestureEnded(gesture: gesture) {
canvasView.showHorizontalSnapIndicators(areVisible: false)
canvasView.showVerticalSnapIndicators(areVisible: false)
if (allGesturesEnded()) {
gesturesAlreadyEnded = true
print("Can create transformation command")
}
return
}
}
#objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
if gesture.state == .began {
gesturesAlreadyEnded = false
}
if !gesturesAlreadyEnded && isGestureEnded(gesture: gesture) {
if (allGesturesEnded()) {
gesturesAlreadyEnded = true
print("Can create transformation command")
}
return
}
}
#objc func handleRotation(_ gesture: UIRotationGestureRecognizer) {
if gesture.state == .began {
gesturesAlreadyEnded = false
}
if !gesturesAlreadyEnded && isGestureEnded(gesture: gesture) {
if (allGesturesEnded()) {
gesturesAlreadyEnded = true
print("Can create transformation command")
}
return
}
}
}
extension SCCanvasViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.view != otherGestureRecognizer.view {
return false
}
return true
}
}
Set it up to get multi-touch notifications, then examine the set of touches impacted by the event.
class TouchableView: UIView {
var touchViews = [UITouch:TouchSpotView]()
override init(frame: CGRect) {
super.init(frame: frame)
isMultipleTouchEnabled = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isMultipleTouchEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
createViewForTouch(touch: touch)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let view = viewForTouch(touch: touch)
// Move the view to the new location.
let newLocation = touch.location(in: self)
view?.center = newLocation
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
removeViewForTouch(touch: touch)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
removeViewForTouch(touch: touch)
}
}
// Other methods. . .
}

Adding UIPanGesture to UITableView is blocking scrolling of UITableView in Xcode 11.3

I have UIViewController with UITableView. I want to dismiss the UIViewController when user pull down the table. By adding UIPanGestureRecognizer to UIViewController's view will work only if table contents are less and table is not scrollable. So I added UIPanGestureRecognizer to tableView :
self.detailTableView.bounces = true
let gesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
gesture.delegate = self
self.detailTableView.gestureRecognizers = [gesture]
onPan Method :
#objc func onPan(_ panGesture: UIPanGestureRecognizer) {
guard self.detailTableView.contentOffset.y <= 0 else {
return
}
func slideViewVerticallyTo(_ yPoint: CGFloat) {
self.view.frame.origin = CGPoint(x: 0, y: yPoint)
}
switch panGesture.state {
case .began, .changed:
// If pan started or is ongoing then
// slide the view to follow the finger
let translation = panGesture.translation(in: view)
let yPoint = max(0, translation.y)
slideViewVerticallyTo(yPoint)
case .ended:
// If pan ended, decide it we should close or reset the view
// based on the final position and the speed of the gesture
let translation = panGesture.translation(in: view)
let velocity = panGesture.velocity(in: view)
let closing = (translation.y > self.view.frame.size.height / 2) ||
(velocity.y > minimumVelocityToHide)
if closing {
UIView.animate(withDuration: animationDuration, animations: {
// If closing, animate to the bottom of the view
slideViewVerticallyTo(self.view.frame.size.height)
}, completion: { (isCompleted) in
if isCompleted {
// Dismiss the view when it disappeared
// Dismiss UIViewController here....
}
})
} else {
// If not closing, reset the view to the top
UIView.animate(withDuration: animationDuration, animations: {
slideViewVerticallyTo(0)
})
}
default:
// If gesture state is undefined, reset the view to the top
UIView.animate(withDuration: animationDuration, animations: {
slideViewVerticallyTo(0)
})
}
}
Also implemented following delegate because as tableView bounce property is set to true, table gets bounce in all direction. By enabling only vertical direction bounce was not working.
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let panRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
// Ensure it's a Vertical drag
let velocity = panRecognizer.velocity(in: self.view)
if abs(velocity.y) < abs(velocity.x) {
return false
}
} else {
return false
}
return true
}
This code was working fine with Xcode 10.2.1. Now I have updated to Xcode 11.3, dismiss is working but it has blocked scrolling of tableView.
Can anyone provide the solution?
Thank you in advance.
You can try to handle the gesture recognizers in following methods.
By doing this you can achieve the drag down to dismiss functionality once you find the view gesture attached to and the gesture kind.
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && gestureRecognizer.view == tableview && gestureRecognizer.view == tableview {
// your logic here
}
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && gestureRecognizer.view == tableview && gestureRecognizer.view == tableview {
// your logic here
}
return true
}
}

UIScrollView with subviews taps not being excluded if superview is involved

So I am trying to create an interface in which there are essentially 3 levels of views possible.
The UIView in charge of deciding the meta-data of new cells
The cells themselves
The background UIScrollView
Everything is a child of the UIScrollView however 1 is displayed on top of 2.
The goal is that all the cells are draggable and the scroll view is scrollable. However when the scroll view is tapped that should trigger the creation of a new node and thus the appearance of view 1 which will disappear upon the press of a submit button.
I have set up just about everything except I have one huge problem. If I tap on a cell I still get the creation view meaning that the TapGestureRecognizer isnt excluding subviews. So here is roughly what is set up:
class Node: UILabel {
var recognizer:UIPanGestureRecognizer?
init() {
self.isExclusiveTouch = true
self.isUserInteractionEnabled = true
recognizer = UIPanGestureRecognizer(target: self, action: #selector(drag))
self.addGestureRecognizer(recognizer!)
}
#objc
func drag(rec: UIPanGestureRecognizer) {
goToPoint(point: rec.location(in: self.superview!))
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
var shouldReceive = false
if let clickedView = touch.view {
if clickedView == self {
shouldReceive = true;
}
}
return shouldReceive
}
}
class NodeCreator: UIView {
var submit: UIButton = UIButton(type: UIButton.ButtonType.roundedRect)
init() {
super.init(frame: CGRect.zero)
submit.setTitle("Submit", for: .normal)
submit.translatesAutoresizingMaskIntoConstraints = false
submit.addTarget(self, action: #selector(press), for: .touchUpInside)
self.addSubview(submit)
}
#objc func press() {
if let d = delegate {
if let t = inputField.text {
d.created(title: t, pt: storeP)
self.removeFromSuperview()
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TaskView: UIScrollView, CreationDelecate{
func created(title: String, pt: CGPoint) {
let temp = Node()
view.addNode(temp, at: pt)
}
var tapRecognizer:UITapGestureRecognizer?
var nodeSet:[Node] = []
func addNode(_ n1: Node, at: CGPoint = CGPoint.zero) {
nodeSet.append(n1)
n1.goToPoint(point: at)
self.addSubview(n1)
self.bringSubviewToFront(n1)
self.isScrollEnabled = true
self.isUserInteractionEnabled = true
self.isExclusiveTouch = false
self.canCancelContentTouches = false
if (tapRecognizer == nil) {
tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tap))
self.addGestureRecognizer(tapRecognizer!)
}
}
#objc func tap(rec: UITapGestureRecognizer) {
let pos = rec.location(in: self)
let creator = NodeCreator(pt: pos)
creator.delegate = self
self.addSubview(creator)
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
var shouldReceive = false
if let clickedView = touch.view {
if clickedView == self {
shouldReceive = true;
}
}
return shouldReceive
}
}
Why is the TaskView still receiving tap gestures when the Nodes are all exclusive touch.
Code does not work because you did not assigned delegate for gesture recognizers. Delegate functions are implemented in Node and TaskView classes. But you did not declared classes as delegates i.e.
class TaskView: UIScrollView, CreationDelecate, UIGestureRecognizerDelegate
Than assign delegate to gesture recognizer:
recognizer.delegate = self as! UIGestureRecognizerDelegate
Should work

Switch between gestures on a view with same touch

In UICollectionView I added UIPanGestureRecognizer. At first UIPanGestureRecognizer is disabled, I want to enable it when contentOffset.y of UICollectionView reaches some value during scrolling. I am trying to achieve this by following code. But it works only in second touch on the screen. I want to work with gesture when contentOffset.y is 44 without taking off the finger.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if collectionView.contentOffset.y == CGFloat(44) {
return false
}
return true
}
Yes. According to your logic it happens only on second touch. Try to enable or disable pan gesture in scrollView Delegate method like below.
func scrollViewDidScroll(scrollView: UIScrollView) {
if collectionView.contentOffset.y < CGFloat(44) {
panGesture.enabled = false
}
panGesture.enabled = true
}
In your gesture's began state check the condition
func gestureTap(sender : UIGestureRecognizer){
if sender.state == .began {
if collectionView.contentOffset.y == CGFloat(44) {
} else {
return
}
}
}
Or
In scrollView Delegate method you can check too. Check below
func scrollViewDidScroll(scrollView: UIScrollView) {
if collectionView.contentOffset.y < CGFloat(44) {
yourGesture.enabled = false
} else {
yourGesture.enabled = true
}
}

How can I implement "drag right to dismiss" a View Controller that's in a navigation stack?

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.)
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:
Calculate the percentage of drag on the view
.Begin: Specify which segue to perform and assign UINavigationController delegate. delegate will be needed for InteractiveTransitioning
.Changed: UpdateInteractiveTransition with percentage
.Ended: Continue remaining transitioning if drag 50% or more or higher velocity else cancel
.Cancelled, .Failed: cancel transitioning
References:
UIPercentDrivenInteractiveTransition
https://github.com/visnup/swipe-left
https://github.com/robertmryan/ScreenEdgeGestureNavigationController
https://github.com/groomsy/custom-navigation-animation-transition-demo
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
}
}
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.
if you swipe left it also dismisses just as if you were swiping right.
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
}
}
}
The cleanest way is to subclass your navigation controller and add a directional pan gesture recognizer to its view that borrows its target/action properties from the default interaction pan gesture recognizer.
First, create a directional pan gesture recognizer that simply puts itself into a failed state if the initial gesture is not in the desired direction.
class DirectionalPanGestureRecognizer: UIPanGestureRecognizer {
enum Direction {
case up
case down
case left
case right
}
private var firstTouch: CGPoint?
var direction: Direction
init(direction: Direction, target: Any? = nil, action: Selector? = nil) {
self.direction = direction
super.init(target: target, action: action)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
firstTouch = touches.first?.location(in: view)
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
switch state {
case .possible:
if let firstTouch = firstTouch,
let thisTouch = touches.first?.location(in: view) {
let deltaX = thisTouch.x - firstTouch.x
let deltaY = thisTouch.y - firstTouch.y
switch direction {
case .up:
if abs(deltaY) > abs(deltaX),
deltaY < 0 {
break
} else {
state = .failed
}
case .down:
if abs(deltaY) > abs(deltaX),
deltaY > 0 {
break
} else {
state = .failed
}
case .left:
if abs(deltaX) > abs(deltaY),
deltaX < 0 {
break
} else {
state = .failed
}
case .right:
if abs(deltaX) > abs(deltaY),
deltaX > 0 {
break
} else {
state = .failed
}
}
}
default:
break
}
super.touchesMoved(touches, with: event)
}
override func reset() {
firstTouch = nil
super.reset()
}
}
Then subclass UINavigationController and perform all of the logic in there.
class CustomNavigationController: UINavigationController {
let popGestureRecognizer = DirectionalPanGestureRecognizer(direction: .right)
override func viewDidLoad() {
super.viewDidLoad()
replaceInteractivePopGestureRecognizer()
}
private func replaceInteractivePopGestureRecognizer() {
guard let targets = interactivePopGestureRecognizer?.value(forKey: "targets") else {
return
}
popGestureRecognizer.setValue(targets, forKey: "targets")
popGestureRecognizer.delegate = self
view.addGestureRecognizer(popGestureRecognizer)
interactivePopGestureRecognizer?.isEnabled = false // this is optional; it just disables the default recognizer
}
}
And then conform to the delegate. We only need the first method, gestureRecognizerShouldBegin. The other two methods are optional.
Most apps that have this feature enabled won't work if the user is in a scroll view and it's still scrolling; the scroll view must come to a complete stop before the swipe-to-pop gesture is recognized. This is not how it works with the default recognizer so the last two methods of this delegate (1) allow simultaneous gesturing with scroll views but (2) force the pop recognizer to fail when competing with the scroll view.
// MARK: - Gesture recognizer delegate
extension CustomNavigationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer.view is UIScrollView {
return true
}
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer.view is UIScrollView {
return true
}
return false
}
}
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
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.
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
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)
})
}
}
}
}

Resources