I'm trying to implement user control node with one ThumbStick and two buttons. I've create separate SKSpriteNode with control elements on it and override touch events of parent node to handle user touch.
The problem is that when I start touch screen, touchesMoved get called many times even if I don't move my finger.
Here is my touch events code:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
for touch in touches {
let touchPoint = touch.location(in: self)
touchStatusLabel.text = String(format: "TOUCH BEGAN %#", arguments:[NSStringFromCGPoint(touchPoint)])
if aButton.frame.contains(touchPoint) {
NSLog("A BUTTON PRESSED")
delegate?.controlInputNode(self, beganTouchButtonWithName: aButton.name!)
}
else if bButton.frame.contains(touchPoint) {
NSLog("B BUTTON PRESSED")
delegate?.controlInputNode(self, beganTouchButtonWithName: bButton.name!)
}
else if touchPoint.x < 0 && touchPoint.y < 0 {
NSLog("THUMBSTICK PRESSED")
leftThumbStickNode.touchesBegan([touch], with: event)
leftThumbStickNode.position = pointByCheckingControlOffset(touchPoint)
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
for touch in touches {
let touchPoint = touch.location(in: self)
touchStatusLabel.text = String(format: "TOUCH MOVED %#", arguments:[NSStringFromCGPoint(touchPoint)])
if !aButton.frame.contains(touchPoint) && !bButton.frame.contains(touchPoint) {
if touchPoint.x < 0 && touchPoint.y < 0 {
leftThumbStickNode.touchesMoved([touch], with: event)
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
for touch in touches {
let touchPoint = touch.location(in: self)
touchStatusLabel.text = String(format: "TOUCH ENDED %#", arguments:[NSStringFromCGPoint(touchPoint)])
let node = atPoint(touchPoint)
if node == aButton || node == bButton {
delegate?.controlInputNode(self, endTouchButtonWithName: node.name!)
}
else {
leftThumbStickNode.touchesEnded([touch], with: event)
}
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
NSLog("TOUCH CANCELED")
leftThumbStickNode.touchesCancelled(touches, with: event)
}
You can try comparing the translation and if nothing has changed then don't do anything, so your touchesMoved function will look like this;
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
for touch in touches {
let location = touch.location(in: self)
let previousLocation = touch.previousLocation(in: self)
let translation = CGPoint(x: location.x - previousLocation.x, y: location.y - previousLocation.y)
if translation == CGPoint.zero {
continue
}
touchStatusLabel.text = String(format: "TOUCH MOVED %#", arguments:[NSStringFromCGPoint(location)])
if !aButton.frame.contains(location) && !bButton.frame.contains(location) {
if location.x < 0 && location.y < 0 {
leftThumbStickNode.touchesMoved([touch], with: event)
}
}
}
}
It appears that this problem occurs only during debugging so I assume that it is some debugger issue
Related
When the player is holding down a button to move around and then presses the shoot button, TouchesEnded is called which then cancels the player's movement. Both actions work separately but not when they're both called at the same time.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let touch = touches.first {
let location = touch.location(in: self)
let objects = nodes(at: location)
for node in objects {
if node.name == "leftBtn" {
player.run(player.leftMovement, withKey: "leftMovement")
} else if node.name == "rightBtn" {
player.run(player.rightMovement, withKey: "rightMovement")
} else if node.name == "upBtn" {
let jump = SKAction.applyImpulse(CGVector(dx: 0, dy: 1000), duration: 0.2)
player.run(jump, withKey: "jump")
} else if node.name == "downBtn" {
let downMovement = SKAction.applyImpulse(CGVector(dx: 0, dy: -500), duration: 0.2)
player.run(downMovement, withKey: "downMovement")
} else if node.name == "shootBtn" {
player.shoot()
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
player.removeAction(forKey: "leftMovement")
player.removeAction(forKey: "rightMovement")
player.removeAction(forKey: "jump")
player.removeAction(forKey: "downMovement")
}
I expect both actions to work independently from another, but unfortunately, that is not the case.
This is probably because when you are touching the shoot button, touchesEnded is also being called which will cancel all your movements.
Similar to how you are checking which nodes were touched in your touchesBegan method, you will need to check if the shoot button was pressed in touchesEnded:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let objects = nodes(at: location)
for node in objects {
if ["leftBtn", "rightBtn", "upBtn", "downBtn"].contains(node.name) {
player.removeAction(forKey: "leftMovement")
player.removeAction(forKey: "rightMovement")
player.removeAction(forKey: "jump")
player.removeAction(forKey: "downMovement")
}
}
}
I have a drawing app with a canvas larger than the size of the phone screen. I want to implement scrolling with two fingers and drawing with one finger. So far I can make the scrolling work just fine but when it comes to drawing, the line begins and then the view where the drawing is loses control of the touch such that only the first part of the line is drawn. I think the scrollview takes control back. Dots can be draw just fine.
This is my subclassed UIScrollView
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesBegan(touches, with: event)
} else {
super.touchesBegan(touches, with: event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesEnded(touches, with: event)
} else {
super.touchesEnded(touches, with: event)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesMoved(touches, with: event)
} else {
super.touchesMoved(touches, with: event)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesCancelled(touches, with: event)
} else {
super.touchesCancelled(touches, with: event)
}
}
override func touchesShouldCancel(in view: UIView) -> Bool {
if (type(of: view)) == UIScrollView.self {
return true
}
return false
}
You will need a Long Press Gesture Recognizer connected to your ScrollerView, set with Min Duration 0 seconds, also to recognize only 1 Touch and Cancel touches in view option active.
You can find all these options under the Attributes Inspector on Interface Builder.
Please play a little with the Tolerance settings to fine tune the results.
Is it possible to detect touches and get the location of a touch from a UIViewController which is being currently used as previewingContext view controller for 3D Touch? (I want to change the image of within the preview controller when the touch moves from left to right)
I've tried both touchesBegan and touchesMoved none of them are fired.
class ThreeDTouchPreviewController: UIViewController {
func getLocationFromTouch(touches: Set<UITouch>) -> CGPoint?{
guard let touch = touches.first else { return nil }
return touch.location(in: self.view)
}
//Not fired
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let location = getLocationFromTouch(touches: touches)
print("LOCATION", location)
}
//Not fired
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let location = getLocationFromTouch(touches: touches)
print("LOCATION", location)
}
}
Even tried adding a UIPanGesture.
Attempting to replicate FaceBook's 3D Touch feature where a user can move finger from left to right to change the current image being displayed.
Video for context: https://streamable.com/ilnln
If you want to achive result same as in FaceBook's 3D Touch feature you need to create your own 3D Touch gesture class
import UIKit.UIGestureRecognizerSubclass
class ForceTouchGestureRecognizer: UIGestureRecognizer {
var forceValue: CGFloat = 0
var isForceTouch: Bool = false
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
handleForceWithTouches(touches: touches)
state = .began
self.isForceTouch = false
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
handleForceWithTouches(touches: touches)
if self.forceValue > 6.0 {
state = .changed
self.isForceTouch = true
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = .ended
handleForceWithTouches(touches: touches)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
state = .cancelled
handleForceWithTouches(touches: touches)
}
func handleForceWithTouches(touches: Set<UITouch>) {
if touches.count != 1 {
state = .failed
return
}
guard let touch = touches.first else {
state = .failed
return
}
forceValue = touch.force
}
}
and now you can add this gesture in your ViewController in viewDidLoad method
override func viewDidLoad() {
super.viewDidLoad()
let gesture = ForceTouchGestureRecognizer(target: self, action: #selector(imagePressed(sender:)))
self.view.addGestureRecognizer(gesture)
}
Now you can manage your controller UI in Storyboard. Add cover view above UICollectionView and UIImageView in a center and connect it with IBOutlets in code.
Now you can add handler methods for gesture
func imagePressed(sender: ForceTouchGestureRecognizer) {
let location = sender.location(in: self.view)
guard let indexPath = collectionView?.indexPathForItem(
at: location) else { return }
let image = self.images[indexPath.row]
switch sender.state {
case .changed:
if sender.isForceTouch {
self.coverView?.isHidden = false
self.selectedImageView?.isHidden = false
self.selectedImageView?.image = image
}
case .ended:
print("force: \(sender.forceValue)")
if sender.isForceTouch {
self.coverView?.isHidden = true
self.selectedImageView?.isHidden = true
self.selectedImageView?.image = nil
} else {
//TODO: handle selecting items of UICollectionView here,
//you can refer to this SO question for more info: https://stackoverflow.com/questions/42372609/collectionview-didnt-call-didselectitematindexpath-when-superview-has-gesture
print("Did select row at indexPath: \(indexPath)")
self.collectionView?.selectItem(at: indexPath, animated: true, scrollPosition: .centeredVertically)
}
default: break
}
}
From this point you need to customize your view to make it look in same way as Facebook do.
Also I created small example project on GitHub https://github.com/ChernyshenkoTaras/3DTouchExample to demonstrate it
I'm working with Swift and SpriteKit.
I have the following situation :
Here, each of the "triangles" is a SKShapenode.
My problem is that I would like to detect when someone touches the screen which triangle is being touched.
I assume that the hitbox of all these triangles are rectangles so my function returns me all the hitboxes touched while I only want to know which one is actually touched.
Is there any way to have a hitbox that perfectly match the shape instead of a rectangle ?
Here's my current code :
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
{
let touch = touches.first
let touchPosition = touch!.locationInNode(self)
let touchedNodes = self.nodesAtPoint(touchPosition)
print(touchedNodes) //this should return only one "triangle" named node
for touchedNode in touchedNodes
{
if let name = touchedNode.name
{
if name == "triangle"
{
let triangle = touchedNode as! SKShapeNode
// stuff here
}
}
}
}
You could try to use CGPathContainsPoint with a SKShapeNode instead of nodesAtPoint, which is more appropriate:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
{
let touch = touches.first
let touchPosition = touch!.locationInNode(self)
self.enumerateChildNodesWithName("triangle") { node, _ in
// do something with node
if node is SKShapeNode {
if let p = (node as! SKShapeNode).path {
if CGPathContainsPoint(p, nil, touchPosition, false) {
print("you have touched triangle: \(node.name)")
let triangle = node as! SKShapeNode
// stuff here
}
}
}
}
}
This would be the easiest way of doing it.
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
{
for touch in touches {
let location = touch.locationInNode(self)
if theSpriteNode.containsPoint(location) {
//Do Whatever
}
}
}
The way I do this with Swift 4:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let touchPosition = touch.location(in: self)
let touchedNodes = nodes(at: touchPosition)
for node in touchedNodes {
if let mynode = node as? SKShapeNode, node.name == "triangle" {
//stuff here
mynode.fillColor = .orange //...
}
}
}
I have a single UIButton on a storyboard. The UIButton is connected to a Touch Drag Inside action myTouchDragInsideAction. The action is triggered when a user drags the button from inside (UIControlEventTouchDragInside).
The problem is that the action is triggered after 1 pixel of inside dragging. However 1 pixel is too sensitive and can be triggered with just the slightest finger movement.
#IBAction func myTouchDragInsideAction(sender: UIButton) {
print("Button dragged inside")
}
Question:
How can I extend this action to trigger the inside dragging action only after at least 5 pixels of movement?
You have to create custom button for it. Following CustomButton may help you.
let DelayPoint:CGFloat = 5
class CustomButton: UIButton {
var startPoint:CGPoint?
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if self.startPoint == nil {
self.startPoint = touches.first?.previousLocationInView(self)
}
if self.shouldAllowForSendActionForPoint((touches.first?.locationInView(self))!) {
super.touchesMoved(touches, withEvent: event)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.startPoint = nil
super.touchesEnded(touches, withEvent: event)
}
func shouldAllowForSendActionForPoint(location:CGPoint) -> Bool {
if self.startPoint != nil {
let xDiff = (self.startPoint?.x)! - location.x
let yDiff = (self.startPoint?.y)! - location.y
if (xDiff > DelayPoint || xDiff < -DelayPoint || yDiff > DelayPoint || yDiff < -DelayPoint) {
return true
}
}
return false
}
}
You change the "DelayPoint" as per your req.
Hope this will help you.
My implementation of solution for Swift 3
final class DraggedButton: UIButton {
// MARK: - Public Properties
#IBInspectable var sensitivityOfDrag: CGFloat = 5
// MARK: - Private Properties
private var startDragPoint: CGPoint?
// MARK: - UIResponder
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let firstTouch = touches.first else {
return
}
let location = firstTouch.location(in: self)
let previousLocation = firstTouch.previousLocation(in: self)
if startDragPoint == nil {
startDragPoint = previousLocation
}
if shouldAllowForSendActionForPoint(location: location) {
super.touchesMoved(touches, with: event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
startDragPoint = nil
super.touchesEnded(touches, with: event)
}
// MARK: - Private methods
private func shouldAllowForSendActionForPoint(location: CGPoint) -> Bool {
guard let startDragPoint = startDragPoint else {
return false
}
let xDifferent = abs(startDragPoint.x - location.x)
let yDifferent = abs(startDragPoint.y - location.y)
return xDifferent > sensitivityOfDrag || yDifferent > sensitivityOfDrag
}
}