I have a subclassed UIButton (DragButton) that I would like to be able to perform pan / pinch / rotate on. Instances of DragButton are created programmatically. There may be 1, there may be 20. All depends on the user.
I am experiencing issues in getting these subclassed buttons to properly work with gestures. The main issue is none of the gestures trigger! Within DragButton class, inside the awakeFromNib instance method, the gestures don't work at all. Inside layoutSubviews, the panning works properly the first drag, but each subsequent hold and drag increases the velocity of the movement a considerably amount. (Pinch and Rotate work properly in layoutSubviews though.) It's almost like layoutSubviews is 'doubling up' the gestures on each interaction. But awakeFromNib doesn't work at all. My guess here is the new buttons have not been unarchived yet.
Here is the code in DragButton
class DragButton: UIButton, UIGestureRecognizerDelegate {
override func awakeFromNib() {
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.draggedView(_:)))
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchedView(_:)))
let rotateRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(self.rotatedView(_:)))
panRecognizer.delegate = self
pinchRecognizer.delegate = self
rotateRecognizer.delegate = self
self.isUserInteractionEnabled = true
self.isMultipleTouchEnabled = true
self.addGestureRecognizer(panRecognizer)
self.addGestureRecognizer(pinchRecognizer)
self.addGestureRecognizer(rotateRecognizer)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
superview?.bringSubview(toFront: self)
}
Also in DragButton are the methods for handling the gestures, however I don't think my problem lies in there.
So to recap - I need a subclassed button (DragButton) to be instantiated programmatically, and have it react to all the 3 gestures. The issue with my code above is none of the gestures work with awakeFromNib, however they work faulty with layoutSubviews on the first tap, then each subsequent tap doubles up the velocity of the panning (hence the fault).
Related
I have a problem where I have a UIScrollView and a Header (UIView) inside my main View and my Header is over my UIScrollView as such:
UIView.
|
|- UIScrollView.
|
|- Header. (UIView)
I want my header to be able to detect taps on it, but I also want my scroll view to be able to scroll when I drag over my Header which right now it is not possible because my Header is over it and is blocking the scroll.
To sum up, I want my Header to detect taps but forward scrolls to my UIScrollView.
To tackle this problem I tried multiple things, and here are some of them:
Adding a UIPanGestureRecognizer to my Header so it is able to detect dragging
Adding a UITapGestureRecognizer to my Header so it is able to detect tapping
Setting isUserInteractionEnabled = false when dragging begins so the gesture can be passed to the next UIResponder which in this case is my UIScrollView
Setting isUserInteractionEnabled = true once my dragging has finished so it can again detect tapping
This is the code snippet:
override func viewLoad() {
myScreenEdgePanGestureRecognizer = UIPanGestureRecognizer(target: self, action:#selector(handlePan))
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action:#selector(handleTap(_:)))
headerView.addGestureRecognizer(myScreenEdgePanGestureRecognizer)
headerView.addGestureRecognizer(tapGestureRecognizer)
}
#objc func handlePan(_ sender: UITapGestureRecognizer){
print("dragging")
if headerView.isUserInteractionEnabled{
headerView.isUserInteractionEnabled = false
}
if sender.state == .began {
} else if sender.state == .ended {
headerView.isUserInteractionEnabled = true
}
}
#objc func handleTap(_ sender: UITapGestureRecognizer){
print("tapped")
}
At this point I see how dragging and tapping are being detected just fine, but for some reason isUserInteractionEnabled = false seems to not be changing how the view is behaving. This code is acting as isUserInteractionEnabled is always true no mater what.
Things that I have also tried besides this:
overriding the hitTest function inside UIButton
overriding touchesBegan, touchesMoved, touchesEnded methods overriding next
setting the variable to return ScrollView as the next UIResponder
setting the isExclusiveTouch method in UIButton
changing the isUserInteractionEnabled in every way possible
I was struggling with this problem too, you should try to use methods of UIGestureRecognizerDelegate which allows you to handle simultaneous gestures.
Connect your gesture recognizers delegates e.g. tapGestureRecognizer.delegate = self
Make your ViewController conform this protocol e.g.
extension YourViewController: UIGestureRecognizerDelegate {}
Implement this function:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true }
I have a UITextView to which I have attached a gesture recognizer as follows:
let testTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(textTextViewTapped(gestureRecognizer:)))
testTapGestureRecognizer.cancelsTouchesInView = false
testTapGestureRecognizer.delaysTouchesBegan = false
testTapGestureRecognizer.delaysTouchesEnded = false
if textTextView != nil {
textTextView!.addGestureRecognizer(testTapGestureRecognizer)
}
The selector mentioned above is as follows:
#objc func textTextViewTapped(gestureRecognizer: UIGestureRecognizer) {
print("testTextViewTapped called.")
}
Every time I tap the UITextView, I can see the message above printed on the console. However, the keyboard doesn't appear any more.
I found Apple's doc confusing here:
Here, it says that
A gesture recognizer doesn’t participate in the view’s responder
chain.
which I am interpreting as that any gestures are also sent to the view and up the chain, as is normal.
Later on the same page, it says,
If a gesture recognizer recognizes its gesture, the remaining touches
for the view are cancelled.
which means that if an attached gesture recognizer is able to understand a gesture as the one it is supposed to recognize, then it will prevent it from being delivered to the view to which it is attached. Further, it specifies 3 properties that should be able to stop the gesture recognizer from doing that. I have set all three of them to false in my code, as shown above.
What is actually happening here and how do I allow the UITextView to interpret the touches normally and also be able to use a gesture recognizer?
You could use the UIGestureRecognizerDelegate to make the UITapGestureRecognizer work along the regular UITextView behaviour:
class TestViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tap))
tapGestureRecognizer.delegate = self
textView.addGestureRecognizer(tapGestureRecognizer)
}
#objc private func tap() {
print("tap")
}
}
extension TestViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
The UITextView probably has its own private gesture recognizer to handle when a user taps on it. When this happens it makes the text view the first responder, which causes the keyboard to appear. Gesture recognizers can force other gesture recognizers to fail when they recognize their gesture. (See the docs) Perhaps this is what is happening when you add your tap gesture. It recognizes the tap and thus forces other gestures to fail, which prevents the text view from becoming the first responder.
The best solution is to follow the answer from this question (as was mentioned by #FrancescoDeliro in the comments) and use the delegate calls to know when editing is beginning/ending.
The goal is to implement two pan gestures against the same SCNScene: one with one finger and the other with two fingers.
This code below isn't working. The one-finger pan function never gets invoked even though the gesture is assigned a distinct selector. One-finger pans and two-finger pans both invoke sceneViewPannedTwoFingers.
From reading other questions it seemed like shouldRecognizeSimultaneouslyWithGestureRecognizer might be the answer, but these pans are not happening simultaneously. It should be either a one-finger pan or a two-finger pan, never both at once.
Is it possible to have two pan gestures as described above? If so, what's the right way to do this?
// Handle one-finger pans
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(sceneViewPannedOneFinger))
sceneView.addGestureRecognizer(panRecognizer)
// Handle two-finger pans
let twoFingerPanRecognizer = UIPanGestureRecognizer(target: self, action: #selector(sceneViewPannedTwoFingers))
sceneView.addGestureRecognizer(twoFingerPanRecognizer)
func sceneViewPannedTwoFingers(sender: UIPanGestureRecognizer) {
print("two finger pan!!!")
}
func sceneViewPannedOneFinger(sender: UIPanGestureRecognizer) {
print("one finger pan!!!")
}
You need to add UIGestureRecognizerDelegate to your view controller and set the gesture recognizers delegate to self inside your view controller. Add the shouldRecognizeSimultaneouslyWithGestureRecognizer method returning true for them. Make sure you set minimum and maximum number of touches also for them.
code like this
class MyClass : UIGestureRecognizerDelegate --< Delegate
func gestureRecognizer(_: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool
{
return true
}
I am trying to detect a tap gesture event on CALayers that are a sublayer of a UIScrollView. Here is how I've got the recognizer set up:
In viewDidLoad:
self.tapRecognizer = UITapGestureRecognizer(target: self, action: "tapHandler:")
self.tabScroller.addGestureRecognizer(tapRecognizer)
Tap handling method:
func tapHandler(recognizer: UITapGestureRecognizer) {
var touchLocation = recognizer.locationInView(recognizer.view)
if recognizer.state == UIGestureRecognizerState.Ended {
if let touchedLayer: CALayer = self.tabScroller.layer.hitTest(touchLocation) {
println("\(touchedLayer.name)")
}
}
}
I know that tapHandler is working because it will execute a basic println("") String placed anywhere inside of it.
But println("(touchedLayer.name)") continuously prints nil and I cannot figure out why it is not recognizing a tap above a CALayer.
The CALayers were added to tabScroller using
tabScroller.layer.addSublayer(brushB)
What am I doing wrong?
Your CALayer and UIView with the tap are on two different layers of the view hierarchy. Core Animation is lower level than view’s in fact, UIViews are made up of layers. Therefore, you have to use hitTest, or a like function to determine the layer which you are tapping.
In my ViewController.swift, I have an array that contains custom UIViews. Every time one is created, I add a UIPanGestureRecognizer to it like this:
var panRecognizer = UIPanGestureRecognizer(target: self, action: "detectPan:")
newCard.gestureRecognizers = [panRecognizer]
This links to my detectPan(recognizer: UIPanGestureRecognizer) function, which handles movement. However, since I have multiple objects linked to the function, I'm not sure how I determine from which one the input is coming.
Is there anything like (the nonexistent) recognizer.target that I could use? Should I just handle the panning from within each of the custom UIViews instead?
Any help would be appreciated!
First of all, you should declare your panRecognizer with let.
let panRecognizer = UIPanGestureRecognizer(target: self, action: "detectPan:")
Second of all, you should not set the gestureRecognizers property of any UIView. This is bad practice because UIKit may have already added its own gesture recognizers to that view behind the scenes. If you subsequently remove those recognizers by assigning [panRecognizer] to that property, you may get unexpected behavior. To add your pan gesture recognizer, do this:
newCard.addGestureRecognizer(panRecognizer)
Then, in your detectPan(recognizer: UIPanGestureRecognizer) method you can detect which UIView was panned with the following code:
func detectPan(recognizer: UIPanGestureRecognizer) {
switch recognizer.view {
case self.customViewArray[0]:
// do something
case self.customViewArray[1]:
// do something else
case ... :
// ...
}