I am currently working at a sidescroller game, in which the jump height depends on how long the player presses the right half of the screen.
Everything works just fine, except if the user touches the screen quickly. This causes the jump to be as big as possible.
Am I doing something wrong or is this just a problem with the way SpriteKit works?
How can I solve this problem?
EDIT: Here are all the methods handling touches in my game:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
for touch in touches
{
swiped = false
let location = touch.location(in: cameraNode)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.065)
{
if self.swiped == false
{
if location.x < 0
{
self.changeColor()
}
else
{
self.jump()
}
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
{
for touch in touches {
let location = touch.location(in: cameraNode)
if location.x > 0
{
// Right
thePlayer.endJump()
}
}
}
And then there is also a gesture recognizer handling swiping left and right, with the following handlers:
#objc func swipedRight()
{
if walkstate != .walkingRight
{
walkstate = .walkingRight
}
else
{
boost(direction: 0)
}
swiped = true
}
#objc func swipedLeft()
{
if walkstate != .walkingLeft
{
walkstate = .walkingLeft
}
else
{
boost(direction: 1)
}
swiped = true
}
Hopefully this is enough to describe the problems. The code above is everything I am doing to handle touches.
Well, the problem was, that I am using a DispatchQueue command to call the jumping method after a short delay, in case the user swiped and not tapped. As a result the touchesEnded method gets called before the jump has even started and thus can not be stopped anymore.
To solve this problem I added a boolean variable which is set to true as soon as the player touches the screen and set to false whenever the users finger leaves the screen. In order to jump this variable has to be set to true and thereby the character will not jump after a quick touch anymore.
Related
I am creating an app so that user has to Swipe all the boxes from the screen. The goal is to swipe all the boxes until all boxes are swiped like example below.
So my question is:
Is it better to create the boxes using Stack View or rather draw manually by coordinates on the screen?
How to detect if user has swiped through the boxes (using UIGestureRecognizer)?
Note: When user swiped through the boxes, swiped boxes will turn into other color.
Both stack view or manually should work nicely. I would go with manually in this case but this is just a preference because you might have more power over it. But there is a downside that you need to reposition them when screen size changes. A third option is also a collection view.
The gesture recognizer should be pretty straight forward. You just add it on the superview of these cells and check the location when it moves or and when it starts. A pan gesture seems the most appropriate but it will not detect if user just taps the screen. This may be a feature but if you want to handle all touches you should either use a long press gesture with zero press duration (It makes little sense, I know but it works), or you may simply just override touch methods:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
handleDrag(at: touch.location(in: viewWhereAllMiniViewsAre))
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
handleDrag(at: touch.location(in: viewWhereAllMiniViewsAre))
}
}
func handleDrag(at location: CGPoint) {
// TODO: handle the nodes
}
The gesture recognizer procedure would do something like:
func onDrag(_ sender: UIGestureRecognizer) {
switch sender.state {
case .began, .changed, .ended, .cancelled: handleDrag(at: sender.location(in: viewWhereAllMiniViewsAre))
case .possible, .failed: break
}
}
Now all you need is your data source. An array of all of your items should be enough. Like:
static let rows: Int = 10
static let columns: Int = 10
var nodes: [Node] = {
return Array<Node>(repeating: Node(), count: LoginViewController.rows * LoginViewController.columns)
}()
And a list of all of your mini views:
var nodeViews: [UIView] = { ... position them or get them from stack view or from collection view }
Now the implementation on touch handle:
func handleDrag(at location: CGPoint) {
nodeViews.enumerated().forEach { index, view in
if view.frame.contains(location) {
view.backgroundColor = UIColor.green
nodes[index].selected = true
}
}
}
This is just an example. An easy one and rather a bad one from maintenance perspective at least. In general I would rather have a node view of custom UIView subclass with a reference to a node. Also it should hook using delegate to a Node instance so that the node reports when the selection state changes.
This way you have much cleaner solution when handling touches:
func handleDrag(at location: CGPoint) {
nodeViews.first(where: { $0.frame.contains(location) }).node.selected = true
}
Checking if all are green is then just
var allGreen: Bool {
return !nodes.contains(where: { $0.selected == false })
}
How can I avoid a UIButtons .touchDragEnter and .touchDragExit functions from rapid firing? This question demonstrates the issue perfectly, but the only answer does not describe how to work around it. I'm trying to animate a button when the users finger on the button, and animate it again when their finger slides off. Are there any better ways to do this? If not, how should I stop my animation code from firing multiple times when the users finger is right between an .enter and an .exit state?
You could instead track the location of the touch point itself and determine when the touch point moves in and out of the button
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let point = t.location(in: self)
// moving in to the button
if button.frame.contains(point) && !wasInButton {
// trigger animation
wasInButton = true
}
// moving out of the button
if !button.frame.contains(point) && wasInButton {
// trigger animation
wasInButton = false
}
}
}
wasInButton could be a boolean variable set to true when there is a touch down in the button's frame:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let point = t.location(in: self)
if button.frame.contains(point) {
wasInButton = true
// trigger animation
} else {
wasInButton = false
}
}
This would require you to subclass the button's superview. And since you might not want to animate as soon as the point leaves the button's frame (because the user's finger or thumb would still be covering most of the button), you could instead do the hit test in a larger frame that encapsulates your button.
I have a custom UIGestureRecognizer for a two finger gesture that works perfectly except for it being very picky about how simultaneously the fingers have to touch the iOS-device for touchesBegan to be called with 2 touches. touchesBegan is often called with only one Touch even though I am trying to use two fingers.
Is there any way to make recognition for the number of Touches more forgiving in regards to how simultaneously you have to place your fingers on the touch screen?
I've noticed that a two finger tap is recognized even when you place first one finger and then another much later while still holding the first finger down.
Here is the code for my touchesBegan function:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if touches.count != 2 {
state = .failed
return
}
// Capture the first touch and store some information about it.
if trackedTouch == nil {
trackedTouch = touches.min { $0.location(in: self.view?.window).x < $1.location(in: self.view?.window).x }
strokePhase = .topSwipeStarted
topSwipeStartPoint = (trackedTouch?.location(in: view?.window))!
// Ignore the other touch that had a larger x-value
for touch in touches {
if touch != trackedTouch {
ignore(touch, for: event)
}
}
}
}
For two-finger gestures, touchesBegan is most likely going to be called twice: once you put the first finger on the screen, and once for the second one.
In the state you keep, you should keep track of both touches (or for that matter, all current touches), and only start the gesture once both touches have been received and the gesture's start condition has been met.
public class TwoFingerGestureRecognizer: UIGestureRecognizer {
private var trackedTouches: Set<UITouch> = []
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
for touch in touches {
if self.trackedTouches.count < 2 {
self.trackedTouches.insert(touch)
}
else {
self.ignore(touch, for: event)
}
}
if self.trackedTouches.count == 2 {
// put your current logic here
}
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
self.trackedTouches.subtract(touches)
}
public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
self.trackedTouches.subtract(touches)
}
public override func reset() {
super.reset()
self.trackedTouches = []
}
}
don't worry about touchesBegan:withEvent: instead use touchesEnded:withEvent: or touchesMoved:withEvent: if the end state does not contain both fingers, set it as .failed otherwise set it as .ended
tapping the screen with more than one finger simultaneously is impossible, so during touchesMoved:withEvent: you will find two fingers. I'm not sure about touchesEnded:withEvent: this one probably won't work since removing two fingers simultaneously is just as hard as applying two fingers simultaneously, but it's worth a try to see how it reacts.
I'd recommend making your code a little more forgiving. Although touchesBegan/Moved/Ended/Cancelled respond to events of "one or more touches" (as stated in the Apple docs) relying on the precision of a user to simultaneously touch the screen with 2 fingers is not ideal. This is assuming you have multi-touch enabled, which it sounds like you do.
Try tracking the touches yourself and executing your logic when you're collection of touches reaches 2. Obviously you'll also have to track when your touch amount becomes more or less and handle things accordingly, but I'd guess you're already handling this if you're gesture is meant to be for 2 fingers only (aside from the extra logic you'd have to add in touchesBegan).
Not sure why the other guys answering with using touchesMoved:withEvent did not answer your question, but maybe you need a github example.
Double touch move example.
Is touchesMoved an option in order for you to achieve the desire outcome? Also you could implement a counter before setting the state to failed
Don't forget to set isMultipleTouchEnabled = true
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if touches.count != 2 {
print("we do NOT have two touches")
if counter > 100 { // 100 "more fogiven"
state = .failed
}
counter += 1
return
} else {
print("we have to two touches")
}
I am currently working on an arcade app where the user taps for the sprite to jump over an obstacle and swipes down for it to slide under an obstacle. My problem is that when I begin a swipe the touchesBegan function is called so the sprite jumps instead of sliding. Is there a way to distinguish these two?
You can use a gestures state to fine tune user interaction. Gestures are coordinated, so shouldn't interfere with each other.
func handlePanFrom(recognizer: UIPanGestureRecognizer) {
if recognizer.state != .changed {
return
}
// Handle pan here
}
func handleTapFrom(recognizer: UITapGestureRecognizer) {
if recognizer.state != .ended {
return
}
// Handle tap here
}
How about using a slight delay for your touch controls? I have a game where I do something similar using a SKAction with delay. Optionally you can set a location property to give your self a bit of wiggle room with the touchesMoved method incase someone has a twitchy finger (thanks KnightOfDragon)
let jumpDelayKey = "JumpDelayKey"
var startingTouchLocation: CGPoint?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
// starting touch location
startingTouchLocation = location
// start jump delay
let action1 = SKAction.wait(forDuration: 0.05)
let action2 = SKAction.run(jump)
let sequence = SKAction.sequence([action1, action2])
run(sequence, withKey: jumpDelayKey)
}
}
func jump() {
// your jumping code
}
Just make sure the delay is not too long so that your controls dont feel unresponsive. Play around with the value for your desired result.
Than in your touches moved method you remove the SKAction if your move threshold has been reached
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
guard let startingTouchLocation = startingTouchLocation else { return }
// adjust this value of how much you want to move before continuing
let adjuster: CGFloat = 30
guard location.y < (startingTouchLocation.y - adjuster) ||
location.y > (startingTouchLocation.y + adjuster) ||
location.x < (startingTouchLocation.x - adjuster) ||
location.x > (startingTouchLocation.x + adjuster) else {
return }
// Remove jump action
removeAction(forKey: jumpDelayKey)
// your sliding code
}
}
You could play around with Gesture recognisers although I am not sure that will work and how it affects the responder chain.
Hope this helps
I am developing a keybaord extension for iOS. On iOS 9 the keys react imediatelly except for keys along left edge of the keyboard. Those react with around 0.2 second delay. The reason is that the touches are simply delivered with this delay to the UIView that is root view of my keyboard. On iOS 8 there is no such delay.
My guess is that this delay is cause by some logic that is supposed to recognize gesture for opening "running apps screen". That is fine but the delay on a keyboard is unacceptable. Is there any way how to get those events without delay? Perhaps just setting delaysTouchesBegan to false on some UIGestureRecognizer?
This is for anyone using later versions of iOS (this is working on iOS 9 and 10 for me). My issue was caused by the swipe to go back gesture interfering with my touchesBegan method by preventing it from firing on the very left edge of the screen until either the touch was ended, or the system recognised the movement to not be that of the swipe to go back gesture.
In your viewDidLoad function in your controller, simply put:
self.navigationController?.interactivePopGestureRecognizer?.delaysTouchesBegan = false
The official solution since iOS11 is overriding preferredScreenEdgesDeferringSystemGestures of your UIInputViewController.
https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys
However, it doesn't seem to work on iOS 13 at least. As far as I understand, that happens due to preferredScreenEdgesDeferringSystemGestures not working properly when overridden inside UIInputViewController, at least on iOS 13.
When you override this property in a regular view controller, it works as expected:
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
return [.left, .bottom, .right]
}
That' not the case for UIInputViewController, though.
UPD: It appears, gesture recognizers will still get .began state update, without the delay. So, instead of following the rather messy solution below, you can add a custom gesture recognizer to handle touch events.
You can quickly test this adding UILongPressGestureRecognizer with minimumPressDuration = 0 to your control view.
Another solution:
My original workaround was calling touch down effects inside hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?, which is called even when the touches are delayed for the view.
You have to ignore the "real" touch down event, when it fires about 0.4s later or simultaneously with touch up inside event. Also, it's probably better to apply this hack only in case the tested point is inside ~20pt lateral margins.
So for example, for a view with equal to screen width, the implementation may look like:
let edgeProtectedZoneWidth: CGFloat = 20
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
guard result == self else {
return result
}
if point.x < edgeProtectedZoneWidth || point.x > bounds.width-edgeProtectedZoneWidth
{
if !alreadyTriggeredFocus {
isHighlighted = true
}
triggerFocus()
}
return result
}
private var alreadyTriggeredFocus: Bool = false
#objc override func triggerFocus() {
guard !alreadyTriggeredFocus else { return }
super.triggerFocus()
alreadyTriggeredFocus = true
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
alreadyTriggeredFocus = false
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
alreadyTriggeredFocus = false
}
...where triggerFocus() is the method you call on touch down event. Alternatively, you may override touchesBegan(_:with:).
If you have access to the view's window property, you can access these system gesture recognizers and set delaysTouchesBegan to false.
Here's a sample code in swift that does that
if let window = view.window,
let recognizers = window.gestureRecognizers {
recognizers.forEach { r in
// add condition here to only affect recognizers that you need to
r.delaysTouchesBegan = false
}
}
Also relevant: UISystemGateGestureRecognizer and delayed taps near bottom of screen