Detecting tap on a UITextView only detects cancellations [duplicate] - ios

This question already has answers here:
How do you make a UITextView detect link part in the text and still have userInteractionDisabled?
(4 answers)
Closed 4 years ago.
I have been struggling to detect a tap on a UITextView with Swift.
My UITextViews are in a table, I must be able to detect links and press them, those links length are unknown.
Also if I tap on the cell, that I don't tap on a link, I want push a new UIViewController on my navigation controller stack.
I tried to create my own textview to overwrite the touchesCancelled, but it wasn't a success. It detects the cancellation which isn't considered a tap on the real device.
The bug doesn't occur in the simulator, but it seems I can't tap on the real device, only long press will work.
class LinkTextView: UITextView {
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
self.textViewWasTapped(self)
}
}
I tried adding directly a gesture recognizer. I didn't have any success there either. It doesn't call the gesture recognizer at all.
I added the UIGestureReconizer delegate to my UIViewController and those lines in my
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var singleTap : UIGestureRecognizer = UIGestureRecognizer(target: self, action: "tapTextView:")
singleTap.delegate = self
cell.mTextOutlet.attributedText = myMutableString
cell.mTextOutlet.addGestureRecognizer(singleTap)
In my LinkTextView class :
class LinkTextView: UITextView {
func tapTextView(stapGesture: UIGestureRecognizer){
println("TAPPING1")
}
}
I looked the forums and found this post : post. It suggests to use CHHLinkTextView. I tried to use it but what I want is to detect the link automatically, which normal uitextview actually does.
I did try using the checkBox in interface builder to parse links with the CHHLinkTextView, but it doesn't work. I didn't see anything in the documentation suggesting it could be done.
How should I proceed ?

You're very close with your first attempt in subclassing UITextView, but instead of overriding only touchesCancelled, you'll want to override all of the touches* methods, i.e.
touchesBegan
touchesMoved
touchesEnded
touchesCancelled
In the overridden methods, send the touch down the responder chain by getting the textView's nextResponder(), check that it isn't nil, and call the method on the next responder.
The reason this works is because the UITextView will intercept the touch if it's on a URL and the UIResponder methods will never be called -- try it for yourself by implementing textView:shouldInteractWithURL:inRange in your tableViewCell or wherever, and you'll see that it's called before any touch events are passed along.
This should be the minimum for what you're trying to do (it's repetitive but short):
class PassThroughTextView: UITextView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let next = next {
next.touchesBegan(touches, with: event)
} else {
super.touchesBegan(touches, with: event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let next = next {
next.touchesEnded(touches, with: event)
} else {
super.touchesEnded(touches, with: event)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
if let next = next {
next.touchesCancelled(touches, with: event)
} else {
super.touchesCancelled(touches, with: event)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let next = next {
next.touchesMoved(touches, with: event)
} else {
super.touchesMoved(touches, with: event)
}
}
}
// In case you want to do anything with the URL or textView in your tableViewCell...
class TableViewCell: UITableViewCell, UITextViewDelegate {
#IBOutlet var textView: PassThroughTextView!
func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
println("textView shouldInteractWithURL")
return true
}
}

I did as Ralfonso said without success, but it helped me realize I had a gesture recognizer conflict. Turns out I didn't have to override all 4 methods of the UITextView, I just override the touchesBegan.
What I was doing in the viewDidLoad was add a gesture recognizer to dismiss the keyboard :
var tapOutTextField: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "dismissKeyboard")
self.view.addGestureRecognizer(tapOutTextField)
What didn't make sense and sent me in a wrong direction was that touchesBegan was actually called after a delay (so was touchCancelled), and I could not get the same behaviour as a UIButton. All that because of this gesture recognizer conflict.

Related

UIButton: manually calling touchesBegan does not set isHighlighted

The isHighlighted property on UIButtonis mostly analogous to whether the button is pressed or not.
The Apple docs are vague about it:
Controls automatically set and clear this state in response to
appropriate touch events.
https://developer.apple.com/documentation/uikit/uicontrol/1618231-ishighlighted
Which method is the system calling to set this?
It appears to be touchesBegan, because you can override this method to prevent it.
But, surprisingly, if you manually call touchesBegan it doesn't get set (for example, from a superview-- see code below).
So it seems it's not simple as touchesBegan. I've tried overriding every method I could...pressesBegan, pressesChanged, pressesCancelled, pressesEnded, touchesCancelled, beginTracking, continueTracking, hitTest, etc.
Only touchesBegan had any effect.
Here's how I tried calling touchesBegan from a superview:
class MyView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self) else { return }
for view in self.subviews {
if let button = view as? UIButton {
if button.frame.contains(point) {
button.touchesBegan(touches, with: event)
print("\(button.isHighlighted)") //prints false
}
}
}
}
}

Tracking Position in a custom UIGestureRecognizer

I'm building my own UIGestureRecognizer subclass in order to combine the tap and swipe gestures and track the direction of their swiping motion. In this intended gesture, the user would touch the screen, let their finger bounce fully off the screen one or more times before returning their finger to the screen and dragging; that is a tap followed immediately by a swipe.
I have been able to get the actual gesture portion of this recognizer to work properly, but where I am finding issue is in storing the trail of points where the user's swipe has traveled.
First, I initialize an array of CGPoints as a class member of my custom recognizer class
class UITapSwipeGestureRecognizer: UIGestureRecognizer {
...
var trail: [CGPoint] = []
Then, in touchesBegan(_, with), I first clear the array if not empty, and feed the starting point into the array
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
...
if !trail.isEmpty { // EXC_BAD_ACCESS error thrown here
trail.removeAll(keepingCapacity: true)
}
trail.append(location(in: view?.window))
...
}
I would go on to discuss my usage in touchesMoved(_, with), but this is enough to trigger my problem. Upon the first attempt to access the array, at !trail.isEmpty, I get the following error:
Thread 1 EXC_BAD_ACCESS (code=1, address=0x10)
Do I need to make a thread safe version of this array? If so, how should I go about doing this? Or am I just going about this entirely the wrong way?
I have been able to replicate your error, by adding the Gesture Recognizer through the StoryBoard.
When I add it programatically, I don't get the error.
Here's the Gesture Recognizer class
import UIKit
import UIKit.UIGestureRecognizerSubclass
class MyGestureClass: UIGestureRecognizer
{
var trail: [CGPoint] = []
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
state = .began
if !trail.isEmpty {
trail.removeAll(keepingCapacity: true)
}
trail.append(location(in: view?.window))
print(trail.count) // Just for debugging
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
trail.append(location(in: view?.window))
print(trail.count) // Just for debugging
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches , with: event)
state = .ended
print("Finished \(trail.count)") // Just for debugging
}
}
and here's how I access it from the View Controller
override func viewDidLoad()
{
super.viewDidLoad()
let myRecognizer = MyGestureClass(target: self, action: #selector(self.tap(_:)))
self.view.addGestureRecognizer(myRecognizer)
}
func tap(_ sender: UITapGestureRecognizer)
{
// No need to do anything here, but appears to be required
}

How to Dismiss keyboard when touching away across the entire app

I have multiple textFields in different viewControllers where keyboard is popped up.
I know how to dismiss keyboard when user clicks on a different part of the screen but I don't want to go and hard code it into every corner of my app.
So is there anyway to enforce keyboard getting keyboard dismissed everywhere on the app when the user clicks anywhere on the screen other than keyboard?
I was thinking of extending the UIViewController, but I also have some textFields inside a view that I add as a subview. Perhaps there could be someway that I extend TextField class itself?
I suggest to create a base UIViewController and let each of your ViewControllers inherit it; override touchesBegan method in it:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
view.endEditing(true)
}
OR you can override viewDidLoad -in the base ViewController- and add a UITapGestureRecognizer to the view, as follows:
override func viewDidLoad() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(Base.endEditing))
view.addGestureRecognizer(tapGesture)
}
func endEditing() {
view.endEditing(true)
}
You can also use an extension of a view controller, if you want the keyboard dismissal to apply to all of them:
extension UIViewController {
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
}

3D Touch force gesture activates cell tap on touch end

I have subclassed a UITableView in my app so that I can intercept touch events. I am using this to allow me to provide 3D Touch gestures on the entire view (including on top of the table view).
This works great, however the problem is that using 3D Touch on one of the cells and then releasing your finger activates the cell tap.
I need to only activate the cell tap if there is no force exerted. I should explain that I am fading an image in gradually over the entire screen as you apply pressure.
Here is my subclass:
protocol PassTouchesTableViewDelegate {
func touchMoved(touches: Set<UITouch>)
func touchEnded(touches: Set<UITouch>)
}
class PassTouchesTableView: UITableView {
var delegatePass: PassTouchesTableViewDelegate?
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesMoved(touches, withEvent: event)
self.delegatePass?.touchMoved(touches)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesEnded(touches, withEvent: event)
self.delegatePass?.touchEnded(touches)
}
}
And here are the methods I'm calling from my view controller when the touches end and move:
internal func touchMoved(touches: Set<UITouch>) {
let touch = touches.first
self.pressureImageView.alpha = (touch!.force / 2) - 1.0
}
internal func touchEnded(touches: Set<UITouch>) {
UIView.animateWithDuration(0.2, animations: {
self.pressureImageView.alpha = 0.0
})
}
You could create a boolean named isForceTouch which is set to false in touchesBegan, and then once force touch is detected, set it to true. Then in didSelectRowAtIndexPath just return false if isForceTouch is true. It may need tweaking but that should work.

UIButton touch location inaccurate when UIView Animating/Rotating

Hello all you awesome peeps! I have been having an odd issue where if UIView is animated, in my case rotated, during the animation the subviews touch locations are inaccurate.
For example, if I keep clicking button1, sometimes button2 will be pressed or nothing will be pressed. I've searched on STO and other blogs, trying their methods, but nothing seems to work. My goal is to have buttons clickable when the view is rotating.
I thought it was a UIButton bug or issue, but even with imageViews, and touch delegates or added Gestures, I get the same thing. I feel like something simple, like the presentation.layer or something that needs to be added to the UIView animate function, but I'm kinda at a loss. Hope you can shed some insight! Thank you all in advance.
func updateRotateView ()
{
UIView.animateWithDuration(2,
delay: 0.0,
options: [UIViewAnimationOptions.CurveLinear, UIViewAnimationOptions.AllowUserInteraction],
animations: {
self.rotationView.transform = CGAffineTransformRotate(self.rotationView.transform, 3.1415926)
//Test A
//self.button1.frame = self.rotationView.convertRect(self.button1.frame, fromView:self.rotationView)
//self.button1.layer.frame = self.rotationView.layer.convertRect(self.button1.layer.frame, fromLayer: self.rotationView.layer)
}
completion: {finished in })
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
{
//Test B
let firstTouch = touches.first
let firstHitPoint = firstTouch.locationInView(self.rotationView)
if (self.button1.frame.contains(firstHitPoint)) {
print("Detected firstHit on Button1")
}
if (self.button1.layer.frame.contains(firstHitPoint)) {
print("Detected firstHit on Button1.layer")
}
if ((self.button1.layer.presentationLayer()?.hitTest(firstHitPoint!)) != nil) {
print("Detected button1.layer hit test")
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
//Test C
let lastTouch = touches.first!
let lastHitPoint = lastTouch.locationInView(self.rotationView)
if (self.button1.frame.contains(lastHitPoint)) {
print("Detected lastHit on Button1")
}
if (self.button1.layer.frame.contains(lastHitPoint)) {
print("Detected lastHit on Button1.layer")
}
if ((self.button1.layer.presentationLayer()?.hitTest(lastHitPoint!)) != nil) {
print("Detected button1.layer hit test")
}
}
extension UIButton {
//Test D
public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView?
{
print("hitTest called")
}
public override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool
{
print("pointInside called")
}
}
Test A -> rotationView.presentationLayer value changes, but the buttons do not, and setting it to an updated frame, is not working as the button1 frame or layer.frame size values do not change.
Test B/C -> UIButton does not respond to touch delegates, but even with imageViews does not hit accurately. Also did try Gestures with same result.
Test D -> Hitpoint does get called, but most likely on the wrong button as well. Point inside only gets called few times and if I am able to press the correct button, but again very inaccurate.
Because you are animating a view. You should know that there is a presentation layer and a model layer when animating.
Once self.rotationView.transform = CGAffineTransformRotate(self.rotationView.transform, 3.1415926)
is called in the animation block, the transform of rotationView has already been set, and the animation you see is just the presentation layer(It is an id type but it should be CALayer).

Resources