I recently started using scenekit for scenekit in iOS 8. I am facing difficulty in detecting whether the user has tapped or pressed on the object. Is there any way to do that?
See the documentation for the hitTest method. Call that from wherever you're handling touch events to get a list of 3D scene objects/locations "under" a 2D screen point.
An easy way to get sample code that shows the hitTest in action is to create a sample app using the Game template in XCode6. Create a new project, select the "Game" template.
The hitTest code should be there in the implementation of:
- (void) handleTap:(UIGestureRecognizer*)gestureRecognize
Add a tap gesture to the object and check whether it is a SCNNode()
#objc func tapGestureRec(sender: UIPanGestureRecognizer? = nil){
let location: CGPoint = (sender?.location(in: self.view))!
let hits = self.sceneKitView.hitTest(location, options: nil)
if let tappedNode : SCNNode = hits.first?.node {
...
}
}
Related
I'm writing an app for Apple Watch using SpriteKit, so I don't have access to functions like touchesBegan and I have to use a WKTapGestureRecognizer to detect taps, no big deal, but I have issues detecting taps on a node.
In my InterfaceController I have:
#IBAction func handleTap(tapGestureRecognizer: WKTapGestureRecognizer){
scene?.didTap(tapGesture: tapGestureRecognizer)
}
And in my Scene file I have
func didTap(tapGesture:WKTapGestureRecognizer) {
let position = tapGesture.locationInObject()
let hitNodes = self.nodes(at: position)
if hitNodes.contains(labelNode) {
labelNode.text = "tapped!"
}
Problem is the Tap Gesture Recognizer gives me the absolute coordinates of the touch point (for example 11.0, 5,0) while my node is positioned relatively to the center of the screen (so its position is -0.99,-11.29 even though is at the center of the screen) therefore the tap is hitting the node not when actually tapping it, but when I tap on the top left of the screen. I searched everywhere and it looks like this is the way to do it yet I don't find people having the same issues. The node has been added via the editor. What am I doing wrong?
So you have the right idea. You are getting this wrong because hitNodes is an array of SKNodes. Those are newly created. So when you use hitNodes.contains the addresses of the labelNode and the address of the newly created SKNode that is being compared would be completely different. Therefore it would never be tapped.
Here's what I would do. This would be in my Scene File. Your InterfaceController class is correct.
func didTap(tapGesture:WKTapGestureRecognizer) {
let position = tapGesture.locationInObject()
if labelNode.contains(position) {
labelNode.text = "tapped!"
}
}
OR another way would be this. I like this way because you only have one function which would be in the WKInterfaceControlller And you would need no functions in your Scene File.
#IBAction func tapOnScreenAct(_ sender: WKGestureRecognizer) {
if scene.labelNode.contains(sender.locationInObject()) {
scene.labelNode.text = "tapped!"
}
}
Either way, both should work. Let me know if you have any more questions or clarifications.
I have a view controller in which a user can move around UIButton, UISlider, and a custom UIView based control by panning the control around the screen. This view controller is used to create custom layout of control by the user. This is all done by adding PanGestureRecognizer to the UIControl to move the position of the control relative to user's finger location.
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(pan))
panRecognizer.delegate = self;
myslider.addGestureRecognizer(panRecognizer)
~~~~~~~~~~~~
//pan handler method
#objc func pan(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: view)
guard let gestureView = gestureRecognizer.view else {
return
}
//Move the center to user's finger location
gestureView.center = CGPoint(x: gestureView.center.x + translation.x, y: gestureView.center.y + translation.y);
gestureRecognizer.setTranslation(.zero, in: view);
if (gestureRecognizer.state == .ended) {
//Save the new location inside data. Not relevant here.
}
}
This worked fine in iOS 13 and below for all the control i mentioned above (with the UISlider being a bit glitchy but it still responded to the pan gesture and i don't need the value of the uislider anyway so it's safe to ignore). However testing the app in iOS 14 reveals that UISlider completely ignore the PanGesture (proven by adding breakpoint that never got called inside the pan gesture handling method).
I have looked at apple's documentation regarding UISlider and found no change at all related to gesture handling so this must be done deliberately in deeper lever. My question is: is there any way to "force" my custom gesture to be executed instead of UISlider's gesture without the need to create transparent button overlay (which i don't know will work or not) / creating dummy slider just for this ?
Additionally i also added UILongPressGestureRecognizer and UITapGestureRecognizer to the control. Which worked fine on other UIButton but completely ignored by the UISlider in iOS14 (in iOS 13 everything worked fine).
Thanks.
Okay.. I found the answer myself after some more digging and getting cue from "claude31" about making a new clean project and testing from there.
The problem was with the overriden beginTracking function. This UISlider of mine is actually subclassed into a custom class and in there the beginTracking function is overriden, as per code below:
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let percent = Float(touch.location(in: self).x / bounds.size.width)
let delta = percent * (maximumValue - minimumValue)
let newValue = minimumValue + delta
self.setValue(newValue, animated: false)
super.sendActions(for: UIControl.Event.valueChanged)
return true
}
This is to make the slider move immediately to the user finger location if the touch is inside the boundaries of the UISlider (without the need to first touch the thumbTrack and sliding it to the position the user wants).
In iOS 13 this function does not block the gestureRecognizer from getting recognized. However in iOS 14B4, overriding this function, with sendActions(for:) method inside it cause the added gestureRecognizer to be ignored completely, be it pan gesture, long press gesture, or even tap gesture.
For me, the solution is to simply add a state to check whether the pan gesture is required in this view controller or not. Because, luckily, i only need the added gestureRecognizer in view controller that does not require the beginTouch function to be customized and vice versa.
Edit:
Originally i wrote that the cause of the problem is due to always true return value, however, I just reread the documentation and the default return value for this function is also true. So i think the root causes of this problem is the sendActions(for:) method, causing the added gestureRecognizer to be ignored. My Answer above has been edited to reflect this.
so there are some similar questions on here, but they don't really answer the questions/problem I am having.
The following explanation of my project may or may not help, I am adding it here just in case...
I am trying to build my first iOS Game, and I have built a scene using the scene editor. The scene contains a SKNode named "World". There is then a backgroundNode and a foregroundNode that are children of the world node. Everything in the scene right now, all SKSpriteNodes, are children of the backgroundNode.
In my GameScene.swift file I have variables attached to the background and foreground nodes so that I can add children to these nodes as the game progresses.
Hopefully this is clear so far...
Now I have added 5 other *.sks files to my project. These file contain scenes that I have made that will be added as children of the foreground node through code in my GameScene.swift file. The SKSpriteNodes in these scene files are placed in the foreground, but their z-position is less than the z-position of one of the background child nodes. This is because I want to have a box appear behind a light beam (the light beam is apart of the background and the box is added to the foreground). Here is a picture in case I caused any confusion
My problem is this...
I want to tap on the screen using gesture recognizers so that when I tap on the box I can then do some stuff. Trouble is that since the light beam has a greater z-position (cause of the effect I want), every time I use the atPoint(_ p: CGPoint) -> SKNode method to determine what node I tapped on I get returned the light beam node and not the box node.
How do I tap on just the box? I have tried changing the isUserInteractionEnabled property for the lights to false already, and I have tried using touchesBegan as shown in many of the other responses to similar question. I have also tried reading the swift developer documents provided by Apple, but I can't figure this out.
The code for my gesture Recognizers is below:
//variables for gesture recognition
let tapGesture = UITapGestureRecognizer()
let swipeUpGesture = UISwipeGestureRecognizer()
//set up the tap gesture recognizer
tapGesture.addTarget(self, action: #selector(GameScene.tappedBox(_:)))
self.view!.addGestureRecognizer(tapGesture)
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
//handles the tap event on the screen
#objc func tappedBox(_ recognizer: UITapGestureRecognizer) {
//gets the location of the touch
let touchLocation = recognizer.location(in: recognizer.view)
if TESTING_TAP {
print("The touched location is: \(touchLocation)")
}
//enumerates through the scenes children to see if the box I am trying to tap on can be detected (It can be detected using this just don't know how to actually detect it by tapping on the box)
enumerateChildNodes(withName: "//Box1", using: { node, _ in
print("We have found a single box node")
})
//tells me what node is returned at the tapped location
let touchedNode = atPoint(touchLocation)
print("The node touched was: \(String(describing: touchedNode.name))")
//chooses which animation to run based on the game and player states
if gameState == .waitingForTap && playerState == .idle{
startRunning() //starts the running animation
unPauseAnimation()
}else if gameState == .waitingForTap && playerState == .running {
standStill() //makes the player stand still
unPauseAnimation()
}
}
Hopefully this is enough for you guys... If you need some more code from me, or need me to clarify anything please let me know
Please read notes in the code. You can get what you want easily in spriteKit. Hope you get the answer.
#objc func tappedBox(_ recognizer: UITapGestureRecognizer) {
let touchLocation = recognizer.location(in: recognizer.view)
// Before you check the position, you need to convert the location from view to Scene. That's the right location.
let point = (recognizer.view as! SKView).convert(touchLocation, to: self)
print ( getNodesatPoint(point, withName: "whatever Node name" ) )
}
// 2 : This function gives you all nodes with the name you assign. If you node has a unique name, you got it.
/// You can change name to other properties and find out.
private func getNodesatPoint(_ point : CGPoint , withName name: String) -> [SKNode] {
return self.nodes(at: point).filter{ $0.name == name}
}
You want to use nodesAtPoint to do a hit test and get all of your nodes that is associated with the point. Then filter the list to find the box you are looking for.
If you have a lot of layers going on, you also may want to just add an invisible layer on top of your graphics that handles nodes being touched
Your other option is to turn off isUserInteractionEnabled on everything except the boxes and then override the touch events for your boxes, but that would mean you can't use gestures like you are doing now.
The App supports iPad Pro and it has to work with the Apple Pencil. What I would like to do is to differentiate whether the user is using the Apple Pencil or his finger.
Something like:
if( the user is pressing the screen with his finger){
do something
} else if ( the user is pressing the screen with an Apple Pencil){
do something else
}
I found the UITouchTypeStylus attribute but was not able to know how it works.
My main problem is that there are really few examples, and they are written in swift, but I am working in objective C. And I am not able to really understand these samples.
Look at this, this is a function from a sample that I found on the apple developer:
func addPointsOfType(var type: LinePoint.PointType, forTouches touches: [UITouch], toLine line: Line, currentUpdateRect updateRect: CGRect) -> CGRect {
var accumulatedRect = CGRect.null
for (idx, touch) in touches.enumerate() {
let isStylus = touch.type == .Stylus // I think that what I'm looking for is something like that
[...]
}
I don't really have the time to learn swift now, unfortunately... This is blocking me completely.
So if someone could give me some samples in Objective C that will allow me to work with the Apple Pencil, or just a beginning. A tutorial on a web site would be perfect also but I don't think there are any.
To check if a UITouch is using the stylus touch type in Objective-C:
if (touch.type == UITouchTypeStylus) {
// Do stuff
}
If you're not handling touches directly, but using a gesture recognizer, then it is a little more complicated.
You could try adding a second long press gesture recogniser and setting the allowedTouchTypes property on each one to recognise stylus or direct touches:
longPressGestureFinger.allowedTouchTypes = #[#(UITouchTypeDirect)];
longPressGesturePencil.allowedTouchTypes = #[#(UITouchTypeStylus)];
If that doesn't work, you would have to add a delegate to the long press gesture, and in the gestureRecognizer: shouldReceiveTouch: method, check and store the type of touch and use this when the gesture action fires.
The UITouch class in iOS 9.1 has a touch property which returns the type:
typedef enum {
UITouchTypeDirect,
UITouchTypeIndirect,
UITouchTypeStylus // THIS ONE
} UITouchType;
I have a 2d animation on scene.When I try to detect tap or click on the animation using OnMouseDown function it doesn't work.But it works using the following code
Input.GetMouseButtonDown(0)
it works but it detects tap on whole window and if I print the onject name like
Debug.log(this.name);
it prints the name of the animated sprite name.I want to detect tap only on the animated sprite.Please anyone help me..
You need to use Raycasting to detect that. Cast a 2D ray down from the input location and check if it hits something. Here's good info about it.
if (Input.GetMouseButtonDown(0))
{
var hit : RaycastHit2D = Physics2D.Raycast(cam.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
if(hit != null)
{
Debug.Log("object clicked: "+hit.collider.tag);
}
}