I asked this question on the Stack Exchange Game Development Site about how to combine the tap and long hold gesture recognizers, and received the following answer:
The way to deal with this is to set a timer once the person taps the phone. The most user friendly scenario that you'd implement would look something like this:
When you detect a tap, set a timer (t = timeToRepeat)
On each frame, decrease the timer by dt
If the timer reaches zero, move the sprite a tile and reset the timer
If the user releases their finger before the first cycle of the timer, move the sprite one tile
Obviously the amount of time that you set your timer to will determine how fast your sprite moves. There are a few variations on this theme depending on the kind of behavior you want as well. For example, you can move the sprite once immediately upon detecting a tap and ignore step #4.
I agree that this is the way to do it, so I am trying to implement this and have come up with the following code to do so:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//This records the time when the user touches the screen
self.startTime = [NSDate date];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSTimeInterval temp = [self.startTime timeIntervalSinceNow];
NSTimeInterval holdTime = temp * -1;
if(holdTime < self.threshold) {
//Tap
}
else {
//Hold
}
}
This code works but I realized that I should call the timer code while the user is holding down on the screen, not after they finish. So is there a way to call the code in touchesEnded while the user is pressing down?
Technically, if the user keeps his/her finger perfectly still there is no method that is called between them. In practice though, touchesMoved gets called a bunch. You should just use an NSTimer though instead of keeping track of the time yourself
Related
I am writing a game and when a user touches the left or right side of the screen, a "sustain level" is increased. Currently the increase only happens when the user first touches the screen (my code is below). I want the increase to be applied for as long as the user holds their finger on the screen. What do I have to do?
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event{
UITouch *touch = [touches anyObject];
CGPoint touchPoint = [touch locationInView:self.view];
if (self.x >= touchPoint.x){
self.sustain += 1;
}else if (self.x <= touchPoint.x){
self.sustain += 1;
}
}
increased.Currently the sustain level only increase when you first touch the screen, I want the force to be applied for as long as the user holds their finger on the screen
You won't get any messages while the user holds the finger still; your next message will be touchesEnded. So you need to start a timer and just keep increasing the force as desired every time the timer fires, until you get touchesEnded.
You need to figure out how rapidly you want this increase to happen. Then use a timer mechanism to change the value. This could be an NSTimer that fires repeatedly on a certain interval: you start the timer when you get touchesBegan: and stop it when you get touchesEnded:
If you're using SpriteKit, the scene has a built-in timer that triggers its update: method. You can use a flag to indicate that a touch is present and change the "sustain" value in update:.
On iPhone's with 3D touch enabled, there is a feature where long pressing the left hand side of the screen with enough force opens lets you change which app is active. Because of this, when a non-moving touch happens on the left hand side of the screen, the touch event is delayed for a second or two until the iPhone verifies that the user is not trying to switch tasks and is interacting with the app.
This is a major problem when developing a game with SpriteKit, as these touches are delayed by a second every time a user taps/holds their finger on the left edge of the screen. I was able to solve this problem by registering a UILongPressGestureRecognizer in the main Scene of the game, thus disabling TouchesBegan and implementing a custom touches function (used as a selector by the gesture recognizer):
-(void)handleLongPressGesture:(UITapGestureRecognizer *)gesture {
CGPoint location = [gesture locationInView:self.view];
if (gesture.state == UIGestureRecognizerStateBegan)
{
//
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
//
}
else if (gesture.state == UIGestureRecognizerStateEnded)
{
//
}
else if (gesture.state == UIGestureRecognizerStateCancelled)
{
//
}
}
-(void)didMoveToView:(SKView *)view {
/* Setup your scene here */
UILongPressGestureRecognizer *longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleLongPressGesture:)];
longPressGestureRecognizer.delaysTouchesBegan = false;
longPressGestureRecognizer.minimumPressDuration = 0;
[view addGestureRecognizer:longPressGestureRecognizer];
// continue
}
The problem with this is that I would have to implement a gesture recognizer for every touch (including simultaneous ones) that I expect the user to enter. This interferes with any touchesBegan methods as subclasses of SKSpriteNode, SKScene, etc. and kills a lot of functionality.
Is there any way to disable this delay? When registering the gestureRecognizer, I was able to set delaysTouchesBegan property to false. Can I do the same somehow for my SKScene?
To see this issue in action, you can run the default SpriteKit project, and tap (hold for a second or two) near the left hand side of the screen. You will see that there is a delay between when you touch the screen and when the SKShapeNodes are rendered (as opposed to touching anywhere else on the screen).
* Edit 1 *
For those trying to find a way to get around this for now, you can keep the gesture recognizer but set its cancelsTouchesInView to false. Use the gesture recognizer to do everything you need to do until TouchesBegan kicks in (touchesBegan will receive the same touch event about a second after the gesture recognizer recognizes the touch). Once touchesBegan kicks in, you can disable everything happening in the gesture recognizer. This seems like a sloppy fix to me, but it works for now.
Still trying to find a more-or-less formal solution.
I have experienced this as an user and it is really annoying. The only thing that worked for me was to disable the 3D touch. Otherwise the left side of the touchscreen is almost useless.
I am using touchesMoved with a coordinate system to detect and respond to user touches within certain areas of the screen. For example, if I have a virtual keyboard and the user swipes across the keys, it reads the coordinates and responds:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch * touch = [[event allTouches] anyObject];
CGPoint point = [touch locationInView:touch.view];
if(point.y < 333 && point.y > 166 && point.x < 90 && point.x > 20)
{
//do something
}
}
...However, the problem is, if the user slowly drags across the keys, or the border between keys, the method is triggered several times in a row, playing the piano key sound in a stutter.
How can I prevent this stutter? I think setting a minimum delay of 0.25 seconds between each successive if statement triggering would help. Also, this delay would only be for a specific if statement -- I want the user to be able to drag across the keys quickly and trigger different key's if-statement as quick as they want.
Does anyone know how to code something like this?
Try this:
BOOL _justPressed; // Declare this in your #interface
...
- (void)unsetJustPressed {
_justPressed = NO;
}
Then, in your touchesMoved:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
if (_justPressed) {
// A key was just pressed, so do nothing.
return;
}
else {
_justPressed = YES;
// Do stuff here
[self performSelector:#selector(unsetJustPressed)
withObject:nil
afterDelay:0.25];
}
}
This way, you set a variable _justPressed to YES every touchesMoved:withEvent: is called (or within a specific conditional in there, depending on what you want to do), and you use performSelector:withObject:afterDelay: to set _justPressed to NO after a certain time period, and so you can just check whether _justPressed is YES when touchesMoved: is called to ascertain whether it was called recently.
Remember, you don't have to return from the method like in the example above, you can simply use _justPressed to check whether you should play the sound, but still perform your other actions. The example is just to give you a basic idea of what to do.
In my view I'm overriding all the "touches*" methods to let the user draw on the screen. I'm recording the locations.
In addition I have two gesture recognizers on my view to detect single tap and double tap.
If I now move my finger just a little bit and short enough, I will be recording a small "draw" gesture. However when raising the finger, an additional tap gesture will be triggered.
By trial and error I could possibly figure out a minimum time and movement threshold but I'm sure there are smarter ways?
I need to know after how much movement and/or it is save to assume that no tap gesture will trigger.
You can avoid tap gestures. Instead of that you can recognize taps in touch events itself.
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
if(touches.count == 1)
{
if([[touches anyObject] tapCount] == 1)
{
// Do the action here for single tap
}
else if([[touches anyObject] tapCount] == 2)
{
// Do the action here for double tap
}
}
}
And you have to set a global bool variable for check whether user moved the finger on the screen.
BOOL _isMoved;
And make it TRUE in the touch move event
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
_isMoved = YES;
}
Before recording the track, you check whether this flag is TRUE or not? And also dont forget to make the flag to FALSE after saving the track
Hope this will help you :)
I'm trying to play a sound on touchesEnded but I'm having a problem. There are multiple objects that get moved around, so if the below code holds true when any object is moved, it constantly plays the sound. How do I only play it once?
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
if(CGRectContainsRect([image1 frame], [image2 frame])){
[self playsound];
}
}
If you only want it to play for a certain object that calls touchesEnded, you first need to identify that object and then you can just do a quick if-then statement.
If you only want it to play once, then just give it a quick variable like int playCount = 0; and then set it to playCount = 1; after you're done playing and do an if-then statement on that as well (i.e. play it if playCount is 0, don't play it if playCount is 1).