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
}
Related
I have written the following code to detect if touches in my Game View Controller. But, when I perform a segue to another view Controller and since the Game view controller is not dismissed, it detects touch and crashes the app. Is there any way by which I can check for touches only in the Game View Controller using an IF statement to my code given below? Would appreciate your help! Thanks:)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// First touch to start the game
if gameState == .ready {
startGame()
}
if let touchLocation = event?.allTouches?.first?.location(in: self.view) {
// Move the player to the new position
movePlayer(to: touchLocation)
// Move all enemies to the new position to trace the player
moveEnemies(to: touchLocation)
}
}
Try to compare a touch's view with the view of your controller e.g.:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard view == touches.first?.view else {
return
}
...
}
The view controller code below demonstrates my question. It prints some debugging statements in response to touch events and a pinch gesture.
If the gesture recognizer is disabled, and I drag a finger on the screen, everything works as I expected: the controller gets a touchesBegan, a bunch of touchesMoved, and a touchesEnded. But if I enable the gesture recognizer and set its delaysTouchesBegan property to true, and drag a single finger, the controller receives only touchesBegan and touchesEnded calls - no touchesMoved.
The docs for delaysTouchesBegan suggest that touch events will pile up and be delivered after the gesture recognizer fails. Is this the expected behavior - to lose all the touchesMoved events?
class ViewController: UIViewController {
var gr: UIPinchGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
gr = UIPinchGestureRecognizer(target: self, action: #selector(pinched(_:)))
gr.delaysTouchesBegan = true
view.addGestureRecognizer(gr)
}
#IBAction func buttonTapped(_ sender: Any) {
gr.isEnabled = !gr.isEnabled
print("pinch recognizer enabled: \(gr.isEnabled)")
}
#objc func pinched(_ gr: Any?) {
print("pinched")
}
var i = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
i = 0
print("touchesBegan")
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesMoved \(i)")
i += 1
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesEnded")
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesCancelled")
}
}
A pinch gesture recognizer reports changes each time the distance between the fingers change. The distance between the fingers is reported as a scale factor. So you will want to look at the scale factor, not touchesMoved. Was that helpful?
#objc func pinched(_ gr: UIPinchGestureRecognizer) {
if gr.state == .began {
print("began")
}
if gr.state == .changed {
print("scale: \(gr.scale)")
}
if gr.state == .ended {
print("ended")
}
}
Handling Pinch Gestures
Handling Pinch Gestures ( Swift 4 - 2018 )
I have used touchesBegan to provide functionality for my UIButtons and have used a tapped gesture to provide functionality for my main player SKSpriteNode making it jump when triggered.
//Code regarding the UIButton touch
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//touches began is only used for GUI buttons -> not to affect player
for touch: AnyObject in touches {
//We get location of the touch
let locationOfTouch = touch.location(in: self)
if muteButton.contains(locationOfTouch) { //mute the game
timer.invalidate()
audioPlayer.volume = 0
}
//Code regarding the tap
let tap = UITapGestureRecognizer(target: self, action: #selector(GameScene.tapped(gesture:)))
tap.cancelsTouchesInView = false
self.view!.addGestureRecognizer(tap)
......
func tapped(gesture: UIGestureRecognizer) { //used to make the player jump
player.physicsBody!.applyImpulse(CGVector(dx: 0, dy: 60))
player.physicsBody!.affectedByGravity = true */
}
My problem is that when I press on the restartButton the tap gesture is also activated later when the touch ends. Is there anything I can do?
The main issue is that the two separate systems for detecting touches (using gesture recognizers and using the touchesBegan/Moved/Ended methods) are in conflict.
One solution is to enable and disable the gesture recognizer if the touch is inside one of the buttons.
In the touchesBegan method, if the touch is inside a button, disable the tap gesture recognizer:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let locationOfTouch = touch.location(in: self)
if muteButton.contains(locationOfTouch) {
// mute action
tap.isEnabled = false
}
}
}
Then in touchesEnded and touchesCancelled, re-enable the gesture recognizer:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
tap.isEnabled = true
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
tap.isEnabled = true
}
This way, if the touch is inside a button, the tap gesture recognizer will not fire. Whenever any touch is complete, we always re-enable the gesture recognizer in case the next touch is meant to make the player jump.
I have tested this out in an empty project, and it works.
Hopefully that helps! Good luck with your game.
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.
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.