Handling Multiple GestureRecognizers - swift

I've run into an issue understanding UIGestureRecognizers. My goal right now is to have a set of GestureRecognizers to do different tasks, for example:
override func viewDidLoad() {
mainScene = GameScene(size: self.view.bounds.size)
main = view as! SKView
mainScene.panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(shiftView(recognizer:)))
main.addGestureRecognizer(mainScene.panRecognizer)
mainScene.tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(testTap(recognizer:)))
main.addGestureRecognizer(mainScene.tapRecognizer)
mainScene.pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(zoomView(recognizer:)))
main.addGestureRecognizer(mainScene.pinchRecognizer)
This is my game View Controller where I handle actions such as panning around a map, zooming, and tapping on map tiles. But I also want to be able to move sprites with a UITapGestureRecognizer so I also created this in my GameScene:
if startGame == true{
self.startGame()
for node in (self.tempGameBoard.landShipLayer.children as? Array<landship>)! {
node.landShipInteraction = UITapGestureRecognizer(target: self, action: #selector(handleTap(recognizer:)))
parentViewController.view.addGestureRecognizer(node.landShipInteraction)
}
}
The landShip in this case is representative of a sprite on screen that I would like to interact with via gesture recognizers.
My issue is that if I add this second set of recognizers, the tapping action becomes completely unresponsive. I can still zoom and pan, but the tapping behaviors I expect on my map tiles do not occur. I feel as though I am missing some understanding of how the gesture recognizers work.
Any ideas?
Thanks!

The UIGestureRecognizerDelegate has a special function managing simultaneous recognition of several gestures on the same object, that will do the trick.
1) Set your UIViewController to conform UIGestureRecognizerDelegate
2) Implement the following function:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer == mainScene.panRecognizer || gestureRecognizer == mainScene.pinchRecognizer) && otherGestureRecognizer == mainScene.tapRecognizer {
return true
}
return false
}
In this particular example we allow the tap gesture to get triggered simultaneously with panning and pinching.
3) Then just assign the delegates to the pan and pinch gesture recognizers:
override func viewDidLoad() {
// your code...
// Set gesture recognizers delegates
mainScene.panRecognizer.delegate = self
mainScene.pinchRecognizer.delegate = self
}

Related

Can't find which view triggered the tap gesture recognizer in my custom view

I know a lot of questions are answered for the issues like this but in my case, the common answers do not work.
I have a custom view which is a small view that appears above the active textfield to show the clipboard data so if the user wants that data in that textField, taps on my custom view so the clipboard text automatically pastes into that textField.
everything works fine when there is a single textField on the page. but if I add another textField, this custom view can't detect the correct textField to paste the text into it.
here is my code inside my customView to detect current textField
private func addTapGesture() {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction))
tap.cancelsTouchesInView = true
tap.delegate = self
window?.addGestureRecognizer(tap)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view!.frame.contains(subviews.first!.frame) {
tapComple?(true)
} else {
tapComple?(false)
}
return true
}
I know that already other answers on the stackoverflow are saing to use this:
if touch.view == self
But it is not working in my case...
The demo project to see everything.
You need to detect if the rectangle contains the point which you touched. You can achieve that with the code below.
Change your gestureRecognizer(_:shouldRecieve:) method to the following:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if subviews.first?.frame.contains(touch.location(in: subviews.first)) == true {
print(true)
tapComple?(true)
} else {
print(false)
tapComple?(false)
}
return true
}
Result:
You don't need to do any calculations or listening gesture recognizer delegate.
Instead of that you can add tap gesture recognizer on your own view, on on the window. So all taps it receive - will be correct ones.
Also right now you're adding your view as subview to text field. This is really a bad practice and will stop this view from receiving taps.
Instead of that I suggest you adding it to to UIViewController view. To do so you need to pass two views into your LContextView: superview and anchorView on which you'll bind your constraints
private func configView(superView: UIView, anchorView: UIView, size: CGSize) {
removePreviusContexes(superView: superView)
superView.addSubview(self)
backgroundColor = .white
translatesAutoresizingMaskIntoConstraints = false
widthAnchor.constraint(equalToConstant: size.width).isActive = true
heightAnchor.constraint(equalToConstant: size.height).isActive = true
centerXAnchor.constraint(equalTo: anchorView.centerXAnchor).isActive = true
centerYAnchor.constraint(equalTo: anchorView.centerYAnchor, constant: -50).isActive = true
...
private func addTapGesture() {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction))
addGestureRecognizer(tap)
}

How to single out and action a UITapGestureRecognizer for a UIView that is in the UIViewController.view that also has a UITapGestureRecognizer?

I have a UIView object that I have added a UITapGestureRecognizer. The main view also has a UITapGestureRecognizer that i use to hide the keyboard. How can i prioritise the tapGesture on the child UIView to do what i need to do? I would like, when tapping the child view to trigger the action associated with this tap, rather than the main views's gesture.
I have tried adding a double tap to the child view, to distinguish between the two gestures, but this just triggers the action for the main views's tap action.
var childView: UIView!
/**Tap screen event listener - to hide the keyboard*/
let tapGesture = UITapGestureRecognizer.init(target: self, action: #selector(hideKeyBaord))
self.view.addGestureRecognizer(tapGesture)
let doubleTap = UITapGestureRecognizer.init(target: self, action: #selector(doSomethingElse))
doubleTap.numberOfTouchesRequired = 2
childView.view.addGestureRecognizer(doubleTap)
One way is to implement the shouldReceiveTouch delegate method for tapGesture:
extension YourViewController : UIGestureRecognizerDelegate {
override func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if gestureRecogniser == tapGesture {
// tapGesture should not receive the touch when the touch is inside the child view
let location = touch.location(in: childView)
return !childView.bounds.contains(location)
} else {
return true
}
}
}
And remember to set the delegate of tapGesture to self.

UISlider and Pan Gesture Recognizer do not mix

As asked here: Pan gesture interferes with UISlider and Gesture problem: UISwipeGestureRecognizer + UISlider I cannot get UISlider to work with a pan gesture where in my navigation controller I have a class that has a "back" button capability.
The answers mention that I should add a delegate to my class as such
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if let view = touch.view, view == slider {
return false
}
return true
}
However, this method never gets called. I have tried about 15 ways to set the delegate for this class, but I honestly have no clue what to set at the delegate.
let myScreenEdgePanGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action:nil)
myScreenEdgePanGestureRecognizer.delegate = self
Nothing to this effect works. Could someone clearly explain how I can fix this problem. I have tried every approach on the first 2 pages of google. Thank you.
If you see this code more precisely, then code is checking class as a UISlider not object of UISlider.
So in swift you have to write like this
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let view = touch.view
if view is UISlider { // check class as a UISlider
return false
}
return true
}
I didn't run and check my code, if any editing required then do and if you need any help then add comment.
Edit
For UIScreenEdgePanGestureRecognizer, check this post :
https://www.hackingwithswift.com/example-code/uikit/how-to-detect-edge-swipes
Try to declare edges
In Swift it will looks like
let myScreenEdgePanGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action:nil)
myScreenEdgePanGestureRecognizer.delegate = self
myScreenEdgePanGestureRecognizer.edges = .left //.right, .top, .bottom

Playing a sound with Swipe Gesture on ViewController in Swift 4

I need to know, how I can add a wav sound (page flipping) to a viewController when a Swipe Gesture is activated. I have 5 different View Contrllers, and when the child swipes the gesture, I would like it to make play a file sound (page flipping). I already implemented the Swipe Gestures on all viewControllers (LEFT & RIGHT), but need the sound to go along with the Gesture.
How I would do it. Add the gesture recognizer to the view in code and setup an action for it to call with each swipe. Get the direction in the function in case you want different sounds or behavior.
override func viewDidLoad() {
super.viewDidLoad()
var swipeRight = UISwipeGestureRecognizer(target: self, action: "respondToSwipeGesture:")
swipeRight.direction = UISwipeGestureRecognizerDirection.Right
self.view.addGestureRecognizer(swipeRight)
var swipeLeft = UISwipeGestureRecognizer(target: self, action: "respondToSwipeGesture:")
swipeLeft.direction = UISwipeGestureRecognizerDirection.Left
self.view.addGestureRecognizer(swipeLeft)
}
func respondToSwipeGesture(gesture: UIGestureRecognizer) {
if let swipeGesture = gesture as? UISwipeGestureRecognizer {
switch swipeGesture.direction {
case UISwipeGestureRecognizerDirection.Right:
// Play sound here
case UISwipeGestureRecognizerDirection.Left:
// Play sound here
default:
break
}
}
}

Ordering gesture recognisers

I was wondering if I could get some clarification on an issue because I have found Apple's documentation to be very unclear. I have added an edge pan gesture to a UIScrollView (or more accurately the scrollview of a UIPageViewController) and I have found that the swipe/pan gestures of the scrollview clash with the edge pan gesture that I have added.
edit: As requested below, here is the code I have used to implement the gesture on the scroll view and the delegate functions that I have used.
PageViewController VDL:
override func viewDidLoad(){
super.viewDidLoad()
self.dataSource = self
for eachSubView in self.view.subviews {
if String(describing: type(of: eachSubView)) == "_UIQueuingScrollView" {
let leftEdge = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipeFromLeft(_:)))
leftEdge.edges = .left
leftEdge.delegate = self
eachSubView.addGestureRecognizer(leftEdge)
}
}
}
Handle Swipe From Left Function:
func handleSwipeFromLeft(_ gesture: UIScreenEdgePanGestureRecognizer) {
let percent = gesture.translation(in: gesture.view!).x / gesture.view!.bounds.size.width
if gesture.state == .began {
interactionController = UIPercentDrivenInteractiveTransition()
if self.navigationController!.viewControllers.count > 1 {
self.navigationController?.popViewController(animated: true)
} else {
dismiss(animated: true)
}
} else if gesture.state == .changed {
interactionController?.update(percent / 4.8)
} else if gesture.state == .ended {
if percent > 0.2 && gesture.state != .cancelled {
interactionController?.finish()
} else {
interactionController?.cancel()
}
interactionController = nil
}
}
GestureRecogniserDelegate:
extension ArticleViewPageController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if String(describing: type(of: gestureRecognizer)) == "UIScreenEdgePanGestureRecognizer" {
return false
} else {
return true
}
}
}
I have been reading Apple's documentation about this issue (Coordinating Multiple Gesture Recognisers, Preferring One Gesture Over Another) but their documentation has not helped. In the first of the two documents there is a section that reads:
To prevent the unintended side effects of the default recognition behavior, you can tell UIKit to recognize gestures in a specific order using a delegate object. UIKit uses the methods of your delegate object to determine whether a gesture recognizer must come before or after other gesture recognizers.
This is exactly what I want to achieve as I want to preference the edge swipe over the other gestures of the scrollview. However, that section goes on to talk about achieving this by implementing the UIGestureRecognizerDelegate method shouldRequireFailureOf which I have implemented but since the scrollview's pan gesture does not actually fail until after a finger is lifted, this does nothing to preference the edge gesture.
I have also implemented the shouldRecognizeSimultaneouslyWith method which does resolve the conflict but it also causes the scrollview to scroll during the edge pan.
I would love to be able to do as that excerpt says and have my gestures recognised in a specific order. Any help in achieving this would be very much appreciated.
Thanks!
This is a bit long shot, but to me it looks like that you should set your leftEdge recogniser to delay touch events and make table recogniser to require leftEdge to fail before it can take over.
leftEdge.delaysTouchesBegan = true
tableView.panGestureRecognizer.require(toFail: leftEdge)
For anyone trying to achieve this very niche thing in the future, I have found a work around.
I implemented the UIGestureRecognizerDelegate method shouldRecognizeSimultaneouslyWith like so:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Because I only implemented the delegate on my edge pan gesture (as shown above) it is only called when the edge pan is activated. This stopped the conflict between my edge pan and the scrollview's pan but introduced the problem of the PageViewController paging whilst my edge pan caused a pop back in the navigation stack. To counteract this, I added the following code to my edge gesture function (again, as outlined above) to be called when the gesture state was .begun:
for eachSubView in self.view.subviews {
if String(describing: type(of: eachSubView)) == "_UIQueuingScrollView", let queueScrollView = eachSubView as? UIScrollView {
queueScrollView.isScrollEnabled = false
}
}
Finally, I added the same block to the viewDidAppear function in my PageViewController except I set queueScrollView.isScrollEnabled = true. This meant that even if the pop gesture was cancelled, paging between views would still work.
This isn't a fantastic solution but it does have the intended effect of prioritising the edge gesture over the pan gesture, just in a very inelegant way. If a better answer comes up, I will edit this post.