Im using Xcode 7, Swift, and SpriteKit and I'm attempting to allow the user to use two fingers at once in my app.
Basically I have two halves to my screen, and I want separate touch-recognition for each side, and simultaneously.
Here is my current code below :
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
{
guard let touch = touches.first else {
return;
}
let location = touch.locationInNode(self)
let touchedNode = self.nodeAtPoint(location)
if (touchedNode == touch1){
//code1
}
else if (touchedNode == touch2){
//code2
}
}
touch1 and touch2 are SkSpriteNodes that each take up a different half of the screen.
This code works well, as long as you only have 1 finger on the screen at a time.
However if there are two(1 for each half), which ever one was placed on the screen first is the one that is registered.
How do I make it so that both are being registered, and therefore code1 and code2 are being run?
You need multipleTouchEnabled property set to true. From the docs about this property :
When set to YES, the receiver receives all touches associated with a
multi-touch sequence. When set to NO, the receiver receives only the
first touch event in a multi-touch sequence. The default value of this
property is NO.
EDIT:
Based on your comments, you might try this (making sprites responsive to touches):
class Button:SKSpriteNode {
init(size:CGSize, color:SKColor) {
super.init(texture: nil, color: color, size: size)
userInteractionEnabled = true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let name = self.name {
print("Button with \(name) pressed")
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let name = self.name {
print("Button with \(name) pressed")
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let name = self.name {
print("Button with \(name) released")
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
let left = Button(size: CGSize(width: frame.size.width/2.0, height: frame.size.height), color: .blackColor())
left.name = "left"
left.position = CGPoint(x: left.size.width/2.0, y: frame.midY)
let right = Button(size: CGSize(width: frame.size.width/2.0, height: frame.size.height), color: .whiteColor())
right.name = "right"
right.position = CGPoint(x:frame.maxX-right.size.width/2.0, y: frame.midY)
addChild(left)
addChild(right)
}
}
Related
Update[11/1/2018]:
I try to test the code step by step and found that:
The method touchesEnded was never called for the leftover UIView.
Same for the method touchesCancelled
I am following the Apple Documentation on implementing a multitouch app.
The url is :
https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/handling_touches_in_your_view/implementing_a_multitouch_app
I am using the 4 methods:
- touchesBegan(:with:),
- touchesMoved(:with:),
- touchesEnded(:with:),
- touchesCancelled(:with:)
to create a UIView on the touching location when touchesBegan is called. Update the UIView location when touchesMoved is called. And finally, remove the UIView when touchesEnded and touchedCancelled is called.
The problem is when I am tapping really fast on the screen, the UIView doesn't remove from the superview. The code is basically the same as the code provided in the URL with different graphics. I add another view which offset a little bit from the touch location to have a better look on the touch point.
Example of the leftover UIView
Another Image, same code as the comment below
I don't understand what's wrong and why is this happening. I suspect that touchesEnded() was never called but I don't know why. And I would like to ask is there any method to prevent this from happening (no leftover UIView)?
I am using XCode Version 9.2 (9C40b) and iPad Air 2.
I have try the app on different iPad model and same thing happen.
Part of the codes:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
createViewForTouch(touch: touch)
}
}
func createViewForTouch( touch : UITouch ) {
let newView = TouchSpotView() //TouchSportView is a custom UIView
newView.bounds = CGRect(x: 0, y: 0, width: 1, height: 1)
newView.center = touch.location(in: self)
// Add the view and animate it to a new size.
addSubview(newView)
UIView.animate(withDuration: 0.2) {
newView.bounds.size = CGSize(width: 100, height: 100)
}
// Save the views internally
touchViews[touch] = newView //touchViews is a dict, i.e. touchView[UITouch:TouchSportView]
}
func removeViewForTouch (touch : UITouch ) {
if let view = touchViews[touch] {
view.removeFromSuperview()
touchViews.removeValue(forKey: touch)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
removeViewForTouch(touch: touch)
}
}
For the full code, please see the attached URL for apple documentation.
Many thanks for your generous help!!!!
Tried following code (copy pasted from the link you provided) and it works perfectly both on actual device and simulator:
import UIKit
class ViewController: UIViewController {
override func loadView() {
view = TouchableView()
view.backgroundColor = .white
}
}
class TouchableView: UIView {
var touchViews = [UITouch:TouchSpotView]()
override init(frame: CGRect) {
super.init(frame: frame)
isMultipleTouchEnabled = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isMultipleTouchEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
createViewForTouch(touch: touch)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let view = viewForTouch(touch: touch)
// Move the view to the new location.
let newLocation = touch.location(in: self)
view?.center = newLocation
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
removeViewForTouch(touch: touch)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
removeViewForTouch(touch: touch)
}
}
func createViewForTouch( touch : UITouch ) {
let newView = TouchSpotView()
newView.bounds = CGRect(x: 0, y: 0, width: 1, height: 1)
newView.center = touch.location(in: self)
// Add the view and animate it to a new size.
addSubview(newView)
UIView.animate(withDuration: 0.2) {
newView.bounds.size = CGSize(width: 100, height: 100)
}
// Save the views internally
touchViews[touch] = newView
}
func viewForTouch (touch : UITouch) -> TouchSpotView? {
return touchViews[touch]
}
func removeViewForTouch (touch : UITouch ) {
if let view = touchViews[touch] {
view.removeFromSuperview()
touchViews.removeValue(forKey: touch)
}
}
}
class TouchSpotView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.lightGray
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Update the corner radius when the bounds change.
override var bounds: CGRect {
get { return super.bounds }
set(newBounds) {
super.bounds = newBounds
layer.cornerRadius = newBounds.size.width / 2.0
}
}
}
So, if you are doing something more with it, add that additional code to the question, or if you removed something from the code, let us know. Otherwise, it should work.
EDIT
For completeness I am including a GitHub link to a minimal project, that works for me.
Instead of finding why touchesCancelled and touchesEnded were not called, using event?.allTouches method to check all touch which UITouch is not being handled.
I'm subclassing SKSpritenode (In Swift) to create coloured blocks that can then be dragged around the scene. The subclass is SoundNode.
import SpriteKit
class SoundNode: SKSpriteNode {
init() {
super.init(texture: nil, color: UIColor.blue, size: CGSize(width: 100, height: 100))
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touch began")
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touches moved")
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
self.position = touchLocation
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touch ended")
}
}
in GameScene.swift
import SpriteKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
addSoundBlock()
}
func addSoundBlock() {
let soundBlock = SoundNode()
soundBlock.position = CGPoint(x: 800, y: 800)
addChild(soundBlock)
}
}
This works, sort of.
soundBlock is added, and can be dragged around the scene. But it flickers and sometimes disappears.
I have tried other methods within touchesMoved, none of them effected the jerkiness.
If I don't subclass the touchesBegan, touchesMoved, touchesEnded, and implement the actions in GameScene, then the dragging becomes smooth. But future plans hinge on being able to subclass these.
Xcode 8, Swift 3, iOS10
I created a class method to handle the movement of the subclass of SKSpriteNode, in my case 'Player' and passed it the location of the touch within GameScene.swift.
Class method declaration in Player.swift:
func movePlayer(location: CGPoint) {
//movement code
}
Calling the function in GameScene.swift:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let location = touches.first?.location(in: self) else { return }
player.movePlayer(location: location)
}
Somewhat cleaner, I suppose. And allows for further subclassing.
I had this same issue, with a subclass of SKNode as the child element of a SKNode on the GameScene parent:
GameScene (top level parent)
GridNode (SKNode subclass, child of GameScene)
TileNode (SKNode subclass, child of GridNode)
The touchesMoved method called from TileNode was returning erratic values for touches.first.location when swiping in a straight line:
134.6666259765625
-42.33331298828125
133.6666259765625
-41.6666259765625
132.33331298828125
-40.6666259765625
131.6666259765625
-40.33331298828125
I believe the issue stems from TileNode's size (and therefore position) being effectively 0 from the perspective of the GameScene.
I solved it like this (in the TileNode class):
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
guard let parent = self.parent as? GridNode else { return }
let movingPoint: CGPoint = touch.location(in: parent)
...
}
Note touch.location(in: parent) vs. touch.location(in: self). GridNode has a set position in its GameScene parent, whereas TileNode does not.
Now movingPoint returns smooth values, without the jitter.
Hope this helps someone.
I have sometimes found inconsistent appearance of a sprite (i.e. sometimes visible, sometimes not) when zPosition is not set to ensure that the sprite is in front of other nodes. I think this might be because if two nodes have the same zPosition, then there isn't a way for the graphics engine to ensure which one is on top.
Try setting zPosition to some number which ensures it's in front of other nodes.
For example after:
soundBlock.position = CGPoint(x: 800, y: 800)
add:
soundBlock.zPosition = 1000
I haven't tried running this but may be worth a go?
In my main view in a UIViewController I have a mapView and a another view (Let's say view A) that is above mapView. Both of them have frames equal to self.view.bounds. The view A is a rectangle that is resizable similar to those used to crop images. My goal here is to let the user specify an area on the map. So, I want the user to be able to zoom in an out of the map as well as change the rectangle width and height proportions since only letting the view A to be an unrealizable square would limit it too much.
I got this project from GitHub https://github.com/justwudi/WDImagePicker from which I am using the resizable rectangle functionality. In the second picture of the Github link, there's a rectangle with 8 dots and a shaded area outside. I want to be able to let the touch pass to the map which is behind the view A if the user touches on the shaded area. Only if the user clicks on the area inside the dots or on the dots (so that he wants to resize the rectangle) I want the view A to recognize the touch. So, I modified the code on touch on the view A and have this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if cropBorderView.frame.contains(touch.location(in: self)){
print("touch contains - touchesbegan")
//self.isUserInteractionEnabled = true
}
else{
print("Touch does not contain - touchesbegan")
self.touchesCancelled(touches, with: event)
//return
}
let touchPoint = touch.location(in: cropBorderView)
anchor = self.calculateAnchorBorder(touchPoint)
fillMultiplyer()
resizingEnabled = true
startPoint = touch.location(in: self.superview)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("inside touches moved")
if let touch = touches.first {
if cropBorderView.frame.contains(touch.location(in: self)){
print("touch contains - touchesmoved")
//self.isUserInteractionEnabled = true
}
else{
print("Touch does not contain - touchesmoved ")
self.touchesCancelled(touches, with: event)
//return
}
if resizingEnabled! {
self.resizeWithTouchPoint(touch.location(in: self.superview))
}
}
}
It is indeed recognizing the touch when I click inside and outside as I wanted, but it is not stopping the touch when I click outside. This means calling self.touchesCancelled(touches, with: event) is not working. Calling return gives a crash and does not work as well. Are there any solutions to this problem?
Thank you for your time and consideration.
touchesCancelled(_:with:) just a notification for UITouch, it will not work this way.
As far as I understand, you implemented touch handlers in your overlay UIView, if so, you can try to replace the call to self.touchesCancelled(touches, with: event) with cancelTracking(with:) function from UIControl class implementation:
else {
print("Touch does not contain - touchesmoved ")
self.cancelTracking(with event)
}
Update solution, based on hitTest:
I've checked possible solutions and it seems that you can use hitTest: to avoid unnecessary touch recognitions. The following example is Swift Playground, you can tap and drag touches and see what happens in the console:
import UIKit
import PlaygroundSupport
class JSGView : UIView {
var centerView = UIView()
override func didMoveToSuperview() {
frame = CGRect(x: 0, y: 0, width: 320, height: 480)
backgroundColor = UIColor.clear
centerView.frame = CGRect(x: 110, y: 190, width: 100, height: 100)
centerView.backgroundColor = UIColor.blue
addSubview(centerView)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
dump(event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if (hitTest(touch.location(in: self), with: event) != nil) {
print("Touch passed hit test and seems valid")
super.touchesCancelled(touches, with: event)
return
}
}
print("Touch isn't passed hit test and will be ignored")
super.touchesMoved(touches, with: event)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if centerView.bounds.contains(centerView.convert(point, from: self)) {
return centerView
}
return nil
}
}
class JSGViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
let customView = JSGView()
view.addSubview(customView)
}
}
let controller = JSGViewController()
PlaygroundPage.current.liveView = controller.view
In SpriteKit, I want to catch event when touch moves over a sprite, but hasn't actually started on this sprite, but on another piece of SKScene.
I can catch touchesBegan inside the SKSpriteNode A if the touch starts on it and then dragged over it, but not when touch started on another node - B - and then dragged over my node - A. Anyone knows how to catch this one, because I think I am doing something wrong here.
try this:
sorry this is swift.. but you can easily do the same thing in obj c
import SpriteKit
class GameScene: SKScene {
let sprite = SKSpriteNode(color: SKColor.redColor(), size: CGSizeMake(100, 100))
var startedOutsideSprite = true
override init(size: CGSize) {
super.init(size: size)
sprite.position = CGPointMake(size.width/2, size.height/2)
addChild(sprite)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
let location = touch.locationInNode(self)
if !sprite.containsPoint(location) {
startedOutsideSprite = true
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
let location = touch.locationInNode(self)
if sprite.containsPoint(location) && startedOutsideSprite {
print("yayyy")
// your code here
}
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
startedOutsideSprite = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Thanks #Christian W. but I have a simpleer solution for now, although it is not what I had on mind actually:
Just put this in Scene and catch the touchesMoved inside it, having this code inside it:
SKNode * draggedOverNode = [self nodeAtPoint:location];
[draggedOverNode touchesMoved:touches withEvent:event];
And implement the touchesMoved function inside that object that actually extends the SKNode (which most of your classes will do).
So I am super new to swift and iOS development but not totally new to programming, and was just going through some tutorials, but basically my code looks like this:
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let circle = SKShapeNode(circleOfRadius: 30.0)
circle.position = CGPointMake(100, 200)
addChild(circle)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
let location = touch.locationInNode(self)
let touchedNode = nodeAtPoint(location)
touchedNode.position = location;
}
}
}
When I build this it recognizes and moves the circle when I drag it, but only for about 30 pixels and then I have to touch it again to move it. What am I doing wrong here?
I might be wrong but I believe your finger is leaving the node limits and enters the background. I would set it up as such:
// First make the shape available throughout the code and make sure you have a place to save the touch event for later use.
var circle: SKShapeNode!
var circleTouch: UITouch?
// Create the Shape
override func didMoveToView(view: SKView) {
circle = SKShapeNode(circleOfRadius: 30.0)
circle.position = CGPointMake(100, 200)
addChild(circle)
}
// When a finger is placed on the screen, check if it's in the circle, if it is, keep that touch even in memory so we can reference it later because other fingers could touch other things in the scene.
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
if nodeAtPoint(touch.locationInNode(self)) == circle {
circleTouch = touch as? UITouch
}
}
}
// Check if the touch event that is moving is the one that anchored the circle to our finger, if yes, move the circle to the position of the finger.
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
if circleTouch != nil {
if touch as UITouch == circleTouch! {
let location = touch.locationInNode(self)
circle.position = location
}
}
}
}
// Clean up the touch event when the finger anchoring the circle is raised from the screen.
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
if circleTouch != nil {
if touch as UITouch == circleTouch! {
circleTouch = nil
}
}
}
}
You can also use if let statements in there but I checked for nil instead for clarity.
Tokuriku, thanks so much, you we're not quite right but it got me to the eventual answer, here it is
import SpriteKit
class GameScene: SKScene {
var circleTouch: UITouch?
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let circle = SKShapeNode(circleOfRadius: 40.0)
circle.fillColor = UIColor.blackColor()
circle.position = CGPoint(x: size.width * 0.5, y: size.height * 0.2)
circle.name = "userCircle"
addChild(circle)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
if nodeAtPoint(touch.locationInNode(self)).name == "userCircle" {
circleTouch = touch as? UITouch
}
}
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
if circleTouch != nil {
if touch as UITouch == circleTouch! {
let location = touch.locationInNode(self)
let touchedNode = nodeAtPoint(location)
touchedNode.position = location
}
}
}
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
if circleTouch != nil {
if touch as UITouch == circleTouch! {
circleTouch = nil
}
}
}
}