Imagine a game world that's nothing other than an SKTileMapNode with 10x10 tiles on the screen.
The user touches a tile.
Does SKTileMapNode provide a way to know which tile has been touched? Or do coordinate hunts need be done to determine which tile is in the location of the touch?
Or is there some other way that this should be done?
Using a UITapGestureRecognizer you can retrieve the touched tile using the tileDefinition function from SKTileMapNode.
func handleTapFrom(recognizer: UITapGestureRecognizer) {
if recognizer.state != .ended {
return
}
let recognizorLocation = recognizer.location(in: recognizer.view!)
let location = self.convertPoint(fromView: recognizorLocation)
guard let map = childNode(withName: "background") as? SKTileMapNode else {
fatalError("Background node not loaded")
}
let column = map.tileColumnIndex(fromPosition: location)
let row = map.tileRowIndex(fromPosition: location)
let tile = map.tileDefinition(atColumn: column, row: row)
}
Then if you have added userData in the TilemapEditor, this can be retrieved. Values to include in userData might be cost to move through the tile etc.
let data = tile.userData?.value(forKey: "myKey")
The advantage of using Recognizers is that Tap, Pan and Long Press can be handled cleanly in separate functions that don't interfere with each other. You initialise the gesture recognizor in SKScene.
override func didMove(to view: SKView) {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTapFrom(recognizer:)))
tapGestureRecognizer.numberOfTapsRequired = 1
view.addGestureRecognizer(tapGestureRecognizer)
}
Related
I have an object on SCNScene and I want the user to zoom in/out on specific parts using double tap and
I thought of two options:
Make the camera itself move to that part, similar to that question,
scenekit - zoom in/out to selected node of scene
and It didn't zoom out when I took this approach or even zoom in accurately.
Add camera node in front of each part, so when the user tap on a part it should reposition the default camera of the scene to the configured camera I added, but I was thinking this would affect the performance due to the nodes I keep adding. Should I try this?
This is the code I tried to the first approach.
#objc
internal func handleTapGesture(_ gestureRecognizer: UIGestureRecognizer) {
let hitPoint = gestureRecognizer.location(in: sceneViewVehicle)
let hitResults = sceneViewVehicle.hitTest(hitPoint, options: nil)
if hitResults.count > 0 {
let result = hitResults.first!
let scale = CGFloat(result.node.simdScale.y)
switch gestureRecognizer.state {
case .changed: fallthrough
case .ended:
cameraNode.camera?.multiplyFOV(by: scale)
default: break
}
}
Adding the Gesture
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
tapGesture.numberOfTapsRequired = 2
sceneViewVehicle.addGestureRecognizer(tapGesture)
Zooming for camera
extension SCNCamera {
public func setFOV(_ value: CGFloat) {
fieldOfView = value
}
public func multiplyFOV(by multiplier: CGFloat) {
fieldOfView *= multiplier
}
}
I want to create a sample application that allows the user to get information about continents on a globe when they tap on them. In order to do this, I need to figure out the location where a user taps on an SCNSphere object in a scene (SceneKit). I attempted to do it like this:
import UIKit
import SceneKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
/* Lighting and camera added (hidden)*/
let earthNode = SCNSphere(radius: 1)
/* Added styling to the Earth (hidden)*/
earthNode.name = "Earth"
scene.rootNode.addChildNode(earthNode)
let sceneView = self.view as! SCNView
sceneView.scene = scene
sceneView.allowsCameraControl = true
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
sceneView.addGestureRecognizer(tapGesture)
}
#objc func handleTap(_ gestureRecognize: UIGestureRecognizer) {
// retrieve the SCNView
let sceneView = self.view as! SCNView
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = sceneView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result: SCNHitTestResult = hitResults[0]
print(result.node.name!)
print("x: \(p.x) y: \(p.y)") // <--- THIS IS WHERE I PRINT THE COORDINATES
}
}
}
When I actually run this code however and click on an area on my sphere, it prints out the coordinates of the tap on the screen instead of where I tapped on the sphere. For example, the coordinates are the same when I tap on the center of the sphere, and when I tap it in the center again after rotating the sphere.
I want to know where on the actual sphere I pressed, not just where I click on the screen. What is the best way that I should go about this problem?
In the hitResult, you can get result.textureCoordinates which tells you the point in your map textures. From this point, you are supposed to know the location of your map as the map should have coordinations which was mapped to textures.
#objc func handleTap(_ gestureRecognize: UIGestureRecognizer) {
// retrieve the SCNView
let sceneView = self.view as! SCNView
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = sceneView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result: SCNHitTestResult = hitResults[0]
print(result.node.name!)
print(result.textureCoordinates(withMappingChannel 0)) // This line is added here.
print("x: \(p.x) y: \(p.y)") // <--- THIS IS WHERE I PRINT THE COORDINATES
}
}
In the AR example project of apple there is an option for placing a chair in the room. What do I need to do to place multiple chairs in the code?
Would a simple append function do the trick?
When I tap on the chair option I need the first chair to be placed in the plane. If I tap again the option the chair should be placed once again. And I know I will need a delete function for this too. So how can I detect a long tap by the user?
A basic tap function to add a ball each time you tap the display.
#objc func handleTap(_ gesture: UITapGestureRecognizer) {
let results = self.sceneView.hitTest(gesture.location(in: gesture.view), types: ARHitTestResult.ResultType.featurePoint)
guard let result: ARHitTestResult = results.first else {
return
}
// create a simple ball
let sphereNode = SCNNode(geometry: SCNSphere(radius: 0.2)
// create position of ball based on tap result
let position = SCNVector3Make(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z)
// set position of ball before adding to scene
sphereNode?.position = position
// each tap adds a new instance of the ball.
self.sceneView.scene.rootNode.addChildNode(sphereNode!)
}
If you need the full swift code to get started...take a look at this earlier post adds a cube.scn from a remote url
You can do long press with
#objc func longPress(_ gesture: UILongPressGestureRecognizer) {
}
But its better to just detect you've tapped on an existing sphereNode you want to remove. You could add something like this to the above function.
let tappedNode = self.sceneView.hitTest(gesture.location(in: gesture.view), options: [:])
if !tappedNode.isEmpty {
let node = tappedNode[0].node
node.removeFromParent()
} else {
// add my new node
}
I'm not sure whether this has been asked or not, but I failed to find a solution. I'm implementing panning gesture on a button, but the idea is: the button is fixed to a position, and when the user drags it, a copy of the button is created and moving with the gesture; the original one stays at its initial place (so there'll be 2 buttons in the view). When the panning ends, the new button is used for some processing, and after that it should disappear (the original one stays as it is; so this whole process can repeat). Currently what I have is as below:
private func addPanGesture() {
for btn in self.selectors { //selectors is a list of buttons which needs this gesture
let pan = UIPanGestureRecognizer(target: self, action:#selector(self.panDetected(_:)))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
btn.addGesturerecognizer(pan)
}
}
#objc private func panDetected(_ panGesture: UIPanGestureRecognizer) {
var translation = panGesture.translation(in: view)
panGesture.setTranslation(CGPoint(x: 0, y: 0), in: view)
var newButton = UIButton()
if let initButton = panGesture.view as? UIButton {
print ("Button recognized!") // this msg is printed out
newButton.center = CGPoint(x: initButton.center.x + translation.x, y: initButton.center.y + translation.y)
newButton.setImage(UIImage(named: "somename"), for: .normal)
}
if panGesture.state == UIGestureRecognizerState.began {
self.view.addSubview(newButton)
}
if panGesture.state == UIGestureRecognizerState.ended {
//some other processing
}
if panGesture.state == UIGestureRecognizerState.changed {
self.view.addSubview(newButton)
}
// printed-out msgs show began, ended, changed states have all been reached
}
But the new button doesn't show up in my view. May I know how to solve this?
You need to create and add the new button as a subview only on .began and remove it on .ended.
Therefore you need to keep a reference to the new button.
You are setting the new button's center but not it's size. You might set its .frame.
You do not need to set a translation to the pan gesture. When you get var translation = panGesture.translation(in: view) you get everything you need.
I have wrote the below code for only one button, but if you are going to allow simultaneous dragging of buttons, you would need to keep a list of moving buttons instead of var movingButton: UIButton?
private func addPanGesture() {
let pan = UIPanGestureRecognizer(target: self, action:#selector(self.panDetected(_:)))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
btn.addGestureRecognizer(pan)
}
#objc private func panDetected(_ panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: view)
let initButton = panGesture.view as! UIButton
if panGesture.state == UIGestureRecognizerState.began {
// this is just copying initial button
// this might be overkill
// just make sure you set the frame, title and image of the new button correctly
let initButtonData = NSKeyedArchiver.archivedData(withRootObject: initButton)
let newButton = NSKeyedUnarchiver.unarchiveObject(with: initButtonData) as! UIButton
// we store new button's reference since we will just move it while it is added to view
movingButton = newButton
self.view.addSubview(movingButton!)
}
if panGesture.state == UIGestureRecognizerState.ended {
//some other processing
// when we are done just we just remove it from superview
movingButton!.removeFromSuperview()
}
if panGesture.state == UIGestureRecognizerState.changed {
// at any change, all we need to do is update movingButton's frame
var buttonFrame = initButton.frame;
buttonFrame.origin = CGPoint(x: buttonFrame.origin.x + translation.x, y: buttonFrame.origin.y + translation.y)
movingButton!.frame = buttonFrame
}
}
Hard to say without debugging it, but a few things I see:
You create a new button every time through panDetected, and add it to the view each time. You should only create an add the button in the .began state.
You should use init(frame:) to create your button, and initialize it to the size of the image.
It looks like you're attaching the pan gestures to the buttons. Then you get the pan coordinates in the button's coordinate system, which doesn't make sense. You should be converting the pan gesture to the button's superview's coordinate system, and should not be calling setTranslation except when the pan gesture's state is .began.
You should be setting the button's coordinates to the new location of the pan gesture each time you get a 1st.changed` message.
Using shinobi charts
Looking for examples how to add gesture recognizers (on touch up) to Tick Marks and schart annoations
I see the documentation for interacting with a series data series, but I need to add GestureRecognizers to tick marks and annotation events
I tried this for the tickMark/datapoint labels with no luck:
func sChart(chart: ShinobiChart!, alterTickMark tickMark: SChartTickMark!, beforeAddingToAxis axis: SChartAxis!) {
if let label = tickMark.tickLabel {
//added a gesture recognizer here but it didn't work
}
For the SchartAnnotations no idea how to go about adding one there
I think you're nearly there with labels. I found I just needed to set userInteractionEnabled = true e.g.
func sChart(chart: ShinobiChart!, alterTickMark tickMark: SChartTickMark!, beforeAddingToAxis axis: SChartAxis!) {
if let label = tickMark.tickLabel {
let tapRecognizer = UITapGestureRecognizer(target: self, action: "labelTapped")
tapRecognizer.numberOfTapsRequired = 1
label.addGestureRecognizer(tapRecognizer)
label.userInteractionEnabled = true
}
}
Annotations are a little trickier, as they're on a view below SChartCanvasOverlay (responsible for listening for gestures). This results in the gestures being 'swallowed' before they get to the annotation.
It is possible however, but you'll need to add a UITapGestureRecognizer to your chart and then loop through the chart's annotations to check whether the touch point was inside an annotation. E.g.:
In viewDidLoad:
let chartTapRecognizer = UITapGestureRecognizer(target: self, action: "annotationTapped:")
chartTapRecognizer.numberOfTapsRequired = 1
chart.addGestureRecognizer(chartTapRecognizer)
And then the annotationTapped function:
func annotationTapped(recognizer: UITapGestureRecognizer) {
var touchPoint: CGPoint?
// Grab the first annotation so we can grab its superview for later use
if let firstAnnotation = chart.getAnnotations().first as? UIView {
// Convert touch point to position on annotation's superview
let glView = firstAnnotation.superview!
touchPoint = recognizer.locationInView(glView)
}
if let touchPoint = touchPoint {
// Loop through the annotations
for item in chart.getAnnotations() {
let annotation: SChartAnnotation = item as SChartAnnotation
if (CGRectContainsPoint(annotation.frame, touchPoint as CGPoint)) {
chart.removeAnnotation(annotation)
}
}
}
}