Switch between gestures on a view with same touch - ios

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

Related

How to resolve the conflict of gestures between PDFView and SCrollView [Swift5, iOS 14]

I am creating a PDF viewer for iPads, with which users can read a PDF by scrolling horizontally.
I created the following code to implement the single page view with page change with gestures (while consulting with How to create a single page vertical scrolling PDFView in Swift and elsewhere).
Although this approach works fine most of the time, I realized that gestures are not detected (or called) when a PDF file is zoomed in. Because of this, I cannot go to the next page by swiping the screen. Playing with the extension PDFView {} function I created, I found out that disabling the user interaction in subview enables me to detect the swipe gestures. However, now I cannot scroll the page inside the PDFView. I would appreciate it if you could help me figure out how to fix this.
What I would like to implement is something like ‎PDF Expert (https://apps.apple.com/us/app/pdf-expert-pdf-reader-editor/id743974925), where I can scroll over to the next page horizontally.
Thank very much you for your help in advance!
import UIKit
import PDFKit
//PDF Zoom scale
var scaleOfPdf: CGFloat = 4
extension PDFView {
func disableBouncing(){
for subview in subviews{
if let scrollView = subview as? UIScrollView{
scrollView.bounces = false
return
}
}
class ViewController: UIViewController, UIGestureRecognizerDelegate, UIDocumentPickerDelegate {
#IBOutlet weak var pdfView: PDFView!
override func viewDidLoad(){
super.viewDidLoad()
pdfView.autoresizesSubviews = true
pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight, .flexibleTopMargin, .flexibleLeftMargin]
pdfView.displayDirection = .horizontal
pdfView.displayMode = .singlePage
pdfView.autoScales = true
// setting a color for background
pdfView.backgroundColor = .black
pdfView.document = pdfDocument
// pdfView.usePageViewController(true, withViewOptions: [UIPageViewController.OptionsKey.interPageSpacing: 20])
pdfView.maxScaleFactor = 4.0
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.disableBouncing()
//setting swipe gesture
let leftSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(respondLeftSwipeGesture(_:)))
leftSwipeGesture.direction = [UISwipeGestureRecognizer.Direction.left]
pdfView.addGestureRecognizer(leftSwipeGesture)
let rightSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(respondRightSwipeGesture(_:)))
rightSwipeGesture.direction = [UISwipeGestureRecognizer.Direction.right]
pdfView.addGestureRecognizer(rightSwipeGesture)
}
//setting swipe-gesture
#objc func respondLeftSwipeGesture(_ sender: UISwipeGestureRecognizer) {
print("left swipe was detected")
if pdfView.document == nil { return }
scaleOfPdf = pdfView.scaleFactor
pdfView.goToNextPage(self)
pdfView.scaleFactor = scaleOfPdf
}
#objc func respondRightSwipeGesture(_ sender: UISwipeGestureRecognizer) {
print("right swipe was detected")
if pdfView.document == nil { return }
scaleOfPdf = pdfView.scaleFactor
pdfView.goToPreviousPage(self)
pdfView.scaleFactor = scaleOfPdf
}
}
Gesture Recognizers are working as a chain or pipeline that processes touches - after one (G1) fails, second one (G2) tries to recognize its gesture. Here you have at least 4 recognizers - your 2 ones (left and right), and the 2 scrollView's ones (pan and pinch). I will give the brief solution that covers only scrollView's pan recognizer, if you'll see problems also with pinch - you'll need to follow the same approach.
Let's say G1 is your left recognizer, and G2 is scrollView's pan recognizer.
In order to make G2 process the same touches as G1, they should be told to recognize simultaneously.
Also, the user might move his/her finger a bit horizontally while scrolling vertically, so in that case, you also want scrolling to start only after your G1 gives up on swipe and fails to recognize it.
In order to achieve that, you should add this code to your VC.
override func viewDidLoad(){
super.viewDidLoad()
...
leftSwipeGesture.delegate = self
leftSwipeGesture.cancelsTouchesInView = false
rightSwipeGesture.delegate = self
rightSwipeGesture.cancelsTouchesInView = false
}
optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer == leftSwipeGesture
|| gestureRecognizer == rightSwipeGesture
|| otherGestureRecognizer == leftSwipeGesture
|| otherGestureRecognizer == rightSwipeGesture
}
optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard let _ = gestureRecognizer as? UIPanGestureRecognizer else { return false }
return otherGestureRecognizer == leftSwipeGesture
|| otherGestureRecognizer == rightSwipeGesture
}
optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard let _ = otherGestureRecognizer as? UIPanGestureRecognizer else { return false }
return gestureRecognizer == leftSwipeGesture
|| gestureRecognizer == rightSwipeGesture
}
If UIGestureRecognizerDelegate methods that I added are not getting called, you'll need to create a subclass PDFView, make left/rightSwipeGesture.delegate = pdfView and override in your PDFView subclass its UIGestureRecognizerDelegate methods with this logic.

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

Conflict between ScrollView pan gesture and panGestureRecognizer

I have a UIScrollView in a UIViewController, which is showed modally by a segue, and an additional UIPanGestureRecognizer do dismiss the view controller by pan. This gesture only works if
scrollView.contentOffset.y == 0
The problem is, now two pan gestures conflict with each other, and I can't scroll the view any more.
To solve this I have tried to use gestureRecognizer(_: shouldRecognizeSimultaneouslyWith:) method, returning yes, and also, I've tried to add my custom pan gesture to UIScrollView pan gesture recognizer like this:
scrollView.panGestureRecognizer.addTarget(self, action: #selector(handlePanGesture(_:)))
But these don't solve the problem
If you know how to solve this issue, I would appreciate your help.
EDITED
Here is the code for my pan gesture that dismisses the view controller:
#IBAction func handlePanGesture(_ sender: UIPanGestureRecognizer) {
let percentThreshold: CGFloat = 0.3
if scrollView.contentOffset.y == 0 {
let translation = sender.translation(in: view)
let verticalMovement = translation.y / view.bounds.height
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
guard let interactor = interactor else {return}
switch sender.state {
case .began:
interactor.hasStarted = true
dismiss(animated: true, completion: nil)
case .changed:
interactor.shouldFinish = progress > percentThreshold
interactor.update(progress)
case .cancelled:
interactor.hasStarted = false
interactor.cancel()
case .ended:
interactor.hasStarted = false
interactor.shouldFinish ? interactor.finish() : interactor.cancel()
default:
break
}
}
}
EDITED_2
Here is the code for Interactor:
class Interactor: UIPercentDrivenInteractiveTransition {
var hasStarted = false
var shouldFinish = false
}
P.s. I know that there is a bunch of similar questions but they don't work for me.
To allow scrolling when a UIPanGestureRecognizer is on a ScrollView you need to create a UIGestureRecognizerDelegate that returns true on gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)
If you don't do this, scrolling will not be possible on the ScrollView.
This is done like so:
let scrollViewPanGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
scrollViewPanGesture.delegate = self
scrollView.addGestureRecognizer(scrollViewPanGesture)
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
I'm not sure but you can try adding the ViewController as a UIPanGestureRecognizer delegate of the swipe to dismiss pan gesture and implementing gestureRecognizerShouldBegin(_:);
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return scrollView.contentOffset.y == 0
}
So the gesture to dismiss will start only if the content offset is zero.
Add a subview under the scrollview and add the pan gesture to it instead of adding it to self.view that for sure will conflict with the scrollview's one
You did the right way when implemented gestureRecognizer(_: shouldRecognizeSimultaneouslyWith:)
But you must set the gesture delegate to current View Controller first:
let panGesture = UIPanGestureRecognizer.init(target: self, action: #selector(handlePanGesture(_:)))
panGesture.delegate = self // <--THIS
scrollView.addGestureRecognizer(panGesture)

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

Swift : Pull down to dismiss `UITableViewController`

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

Resources