Conflict between ScrollView pan gesture and panGestureRecognizer - ios

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)

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

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

Moving view controller based on pan gesture in scrollview

Right now I have a scrollView that takes up the entire view controller. The code below is able to move the scrollView around but I want to move the whole view controller around. How would I do that?
override func viewDidLoad() {
pan = UIPanGestureRecognizer(target: self, action: "handlePan:")
self.scrollview.addGestureRecognizer(pan)
}
func handlePan(recognizer:UIPanGestureRecognizer!) {
switch recognizer.state {
case .Changed:
handlePanChanged(recognizer); break
case .Ended:
handlePanTerminated(recognizer); break
case .Cancelled:
handlePanTerminated(recognizer); break
case .Failed:
handlePanTerminated(recognizer); break
default: break
}
}
func handlePanChanged(recognizer:UIPanGestureRecognizer!) {
if let view = recognizer.view {
var translation = recognizer.translationInView(self.view)
println("moving")
view.center = CGPointMake(view.center.x, view.center.y + translation.y);
recognizer.setTranslation(CGPointZero, inView: self.view)
}
}
I've tried different variations of "self.view.center ...." "UIApplication.sharedApplication.rootViewController.view.center.." etc.
I infer from your other question that you want to a gesture to dismiss this view controller. Rather than manipulating the view yourself in the gesture, I'd suggest you use custom transition with a UIPercentDrivenInteractiveTransition interaction controller, and have the gesture just manipulate the interaction controller. This achieves the same UX, but in a manner consistent with Apple's custom transitions paradigm.
The interesting question here is how do you want to delineate between the custom dismiss transition gesture and the scroll view gesture. What you want is some gesture that is constrained in some fashion. There are tons of options here:
If the scroll view is left-right only, have a custom pan gesture subclass that fails if you use it horizontally;
If the scroll view is up-down, too, then have a top "screen edge gesture recognizer" or add some visual element that is a "grab bar" to which you tie a pan gesture
But however you design this gesture to work, have the scroll view's gestures require that your own gesture fails before they trigger.
For example, if you wanted a screen edge gesture recognizer, that would look like:
class SecondViewController: UIViewController, UIViewControllerTransitioningDelegate {
#IBOutlet weak var scrollView: UIScrollView!
var interactionController: UIPercentDrivenInteractiveTransition?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .Custom
transitioningDelegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
// ...
let edge = UIScreenEdgePanGestureRecognizer(target: self, action: "handleScreenEdgeGesture:")
edge.edges = UIRectEdge.Top
view.addGestureRecognizer(edge)
for gesture in scrollView.gestureRecognizers! {
gesture.requireGestureRecognizerToFail(edge)
}
}
// because we're using top edge gesture, hide status bar
override func prefersStatusBarHidden() -> Bool {
return true
}
func handleScreenEdgeGesture(gesture: UIScreenEdgePanGestureRecognizer) {
switch gesture.state {
case .Began:
interactionController = UIPercentDrivenInteractiveTransition()
dismissViewControllerAnimated(true, completion: nil)
case .Changed:
let percent = gesture.translationInView(gesture.view).y / gesture.view!.frame.size.height
interactionController?.updateInteractiveTransition(percent)
case .Cancelled:
fallthrough
case .Ended:
if gesture.velocityInView(gesture.view).y < 0 || gesture.state == .Cancelled || (gesture.velocityInView(gesture.view).y == 0 && gesture.translationInView(gesture.view).y < view.frame.size.height / 2.0) {
interactionController?.cancelInteractiveTransition()
} else {
interactionController?.finishInteractiveTransition()
}
interactionController = nil
default: ()
}
}
#IBAction func didTapDismissButton(sender: UIButton) {
dismissViewControllerAnimated(true, completion: nil)
}
// MARK: UIViewControllerTransitioningDelegate
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissAnimation()
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
class DismissAnimation: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.25
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let from = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let container = transitionContext.containerView()!
let height = container.bounds.size.height
UIView.animateWithDuration(transitionDuration(transitionContext), animations:
{
from.view.transform = CGAffineTransformMakeTranslation(0, height)
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
)
}
}
Personally, I find the notion of having top and bottom screen edge gestures to be a bad UX, so I'd personally change this modal presentation to slide in from the right, and then swiping from left edge to the right feels logical, and doesn't interfere with the built in top pull down (for iOS notifications). Or if the scroll view only scrolls horizontally, then you can just have your own vertical pan gesture that fails if it's not a vertical pan.
Or, if the scroll view only scrolls left and right, you can add your own pan gesture that is only recognized when you pull down by (a) using UIGestureRecognizerDelegate to recognize downward pans only; and (b) again setting the scroll view gestures to only recognize gestures if our pull-down gesture fails:
override func viewDidLoad() {
super.viewDidLoad()
// ...
let pan = UIPanGestureRecognizer(target: self, action: "handlePan:")
pan.delegate = self
view.addGestureRecognizer(pan)
for gesture in scrollView.gestureRecognizers! {
gesture.requireGestureRecognizerToFail(pan)
}
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if let gesture = gestureRecognizer as? UIPanGestureRecognizer {
let translation = gesture.translationInView(gesture.view)
let angle = atan2(translation.x, translation.y)
return abs(angle) < CGFloat(M_PI_4 / 2.0)
}
return true
}
func handlePan(gesture: UIPanGestureRecognizer) {
// the same as the `handleScreenEdgeGesture` above
}
Like I said, tons of options here. But you haven't shared enough of your design for us to advise you further on that.
But the above illustrates the basic idea, that you shouldn't be moving the view around yourself, but rather use custom transition with your own animators and your own interactive controller.
For more information, see WWDC 2013 Custom Transitions Using View Controllers (and also WWDC 2014 A Look Inside Presentation Controllers, if you want a little more information on the evolution of custom transitions).

Imiplenting panGesture on a viewController [duplicate]

Right now I have a scrollView that takes up the entire view controller. The code below is able to move the scrollView around but I want to move the whole view controller around. How would I do that?
override func viewDidLoad() {
pan = UIPanGestureRecognizer(target: self, action: "handlePan:")
self.scrollview.addGestureRecognizer(pan)
}
func handlePan(recognizer:UIPanGestureRecognizer!) {
switch recognizer.state {
case .Changed:
handlePanChanged(recognizer); break
case .Ended:
handlePanTerminated(recognizer); break
case .Cancelled:
handlePanTerminated(recognizer); break
case .Failed:
handlePanTerminated(recognizer); break
default: break
}
}
func handlePanChanged(recognizer:UIPanGestureRecognizer!) {
if let view = recognizer.view {
var translation = recognizer.translationInView(self.view)
println("moving")
view.center = CGPointMake(view.center.x, view.center.y + translation.y);
recognizer.setTranslation(CGPointZero, inView: self.view)
}
}
I've tried different variations of "self.view.center ...." "UIApplication.sharedApplication.rootViewController.view.center.." etc.
I infer from your other question that you want to a gesture to dismiss this view controller. Rather than manipulating the view yourself in the gesture, I'd suggest you use custom transition with a UIPercentDrivenInteractiveTransition interaction controller, and have the gesture just manipulate the interaction controller. This achieves the same UX, but in a manner consistent with Apple's custom transitions paradigm.
The interesting question here is how do you want to delineate between the custom dismiss transition gesture and the scroll view gesture. What you want is some gesture that is constrained in some fashion. There are tons of options here:
If the scroll view is left-right only, have a custom pan gesture subclass that fails if you use it horizontally;
If the scroll view is up-down, too, then have a top "screen edge gesture recognizer" or add some visual element that is a "grab bar" to which you tie a pan gesture
But however you design this gesture to work, have the scroll view's gestures require that your own gesture fails before they trigger.
For example, if you wanted a screen edge gesture recognizer, that would look like:
class SecondViewController: UIViewController, UIViewControllerTransitioningDelegate {
#IBOutlet weak var scrollView: UIScrollView!
var interactionController: UIPercentDrivenInteractiveTransition?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .Custom
transitioningDelegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
// ...
let edge = UIScreenEdgePanGestureRecognizer(target: self, action: "handleScreenEdgeGesture:")
edge.edges = UIRectEdge.Top
view.addGestureRecognizer(edge)
for gesture in scrollView.gestureRecognizers! {
gesture.requireGestureRecognizerToFail(edge)
}
}
// because we're using top edge gesture, hide status bar
override func prefersStatusBarHidden() -> Bool {
return true
}
func handleScreenEdgeGesture(gesture: UIScreenEdgePanGestureRecognizer) {
switch gesture.state {
case .Began:
interactionController = UIPercentDrivenInteractiveTransition()
dismissViewControllerAnimated(true, completion: nil)
case .Changed:
let percent = gesture.translationInView(gesture.view).y / gesture.view!.frame.size.height
interactionController?.updateInteractiveTransition(percent)
case .Cancelled:
fallthrough
case .Ended:
if gesture.velocityInView(gesture.view).y < 0 || gesture.state == .Cancelled || (gesture.velocityInView(gesture.view).y == 0 && gesture.translationInView(gesture.view).y < view.frame.size.height / 2.0) {
interactionController?.cancelInteractiveTransition()
} else {
interactionController?.finishInteractiveTransition()
}
interactionController = nil
default: ()
}
}
#IBAction func didTapDismissButton(sender: UIButton) {
dismissViewControllerAnimated(true, completion: nil)
}
// MARK: UIViewControllerTransitioningDelegate
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissAnimation()
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
class DismissAnimation: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.25
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let from = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let container = transitionContext.containerView()!
let height = container.bounds.size.height
UIView.animateWithDuration(transitionDuration(transitionContext), animations:
{
from.view.transform = CGAffineTransformMakeTranslation(0, height)
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
)
}
}
Personally, I find the notion of having top and bottom screen edge gestures to be a bad UX, so I'd personally change this modal presentation to slide in from the right, and then swiping from left edge to the right feels logical, and doesn't interfere with the built in top pull down (for iOS notifications). Or if the scroll view only scrolls horizontally, then you can just have your own vertical pan gesture that fails if it's not a vertical pan.
Or, if the scroll view only scrolls left and right, you can add your own pan gesture that is only recognized when you pull down by (a) using UIGestureRecognizerDelegate to recognize downward pans only; and (b) again setting the scroll view gestures to only recognize gestures if our pull-down gesture fails:
override func viewDidLoad() {
super.viewDidLoad()
// ...
let pan = UIPanGestureRecognizer(target: self, action: "handlePan:")
pan.delegate = self
view.addGestureRecognizer(pan)
for gesture in scrollView.gestureRecognizers! {
gesture.requireGestureRecognizerToFail(pan)
}
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if let gesture = gestureRecognizer as? UIPanGestureRecognizer {
let translation = gesture.translationInView(gesture.view)
let angle = atan2(translation.x, translation.y)
return abs(angle) < CGFloat(M_PI_4 / 2.0)
}
return true
}
func handlePan(gesture: UIPanGestureRecognizer) {
// the same as the `handleScreenEdgeGesture` above
}
Like I said, tons of options here. But you haven't shared enough of your design for us to advise you further on that.
But the above illustrates the basic idea, that you shouldn't be moving the view around yourself, but rather use custom transition with your own animators and your own interactive controller.
For more information, see WWDC 2013 Custom Transitions Using View Controllers (and also WWDC 2014 A Look Inside Presentation Controllers, if you want a little more information on the evolution of custom transitions).

Resources