I have been using touches began to track as many as 8 touches, and each triggers an event. These touches can occur at the same time, or staggered.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"Touch Began");
NSSet *allTouches = [event allTouches];
for (int i=0; i<allTouches.count; i++) {
UITouch *touch = [[allTouches allObjects] objectAtIndex:i];
if (/*touch inside button in question*/) {
//Trigger the event.
}
}
}
That code works for the multitouch, and it has no problems, EXCEPT: (See if you can guess)
Due to the way allTouches works, it literally gets all of the touches. Because of this, it loops through all of the touches that are currently active when the user starts another touch, and thus triggers the event of one of the buttons twice.
Ex: Johnny is pressing button 1. Event 1 occurs. Johnny leaves his finger on button 1, and presses button 2. Event 2 occurs, BUT button 1 is still a part of allTouches, and so, event 1 is triggered again.
So here's the question: How do I get the new touch?
The same touch object will be returned on subsequent calls to touchesBegan for any continuous touch. So just save each UITouch *touch that you have already handled as begun (and not yet ended), and as you iterate the next time in touchesBegan, skip the ones you've so saved/marked.
Related
I have an issue removing an SKSpriteNode from it's parent when a touch is detected on that SKSpriteNode. Here is the code:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint sceneLocation = [touch locationInNode:self];
if ([_tableDoneButton containsPoint:sceneLocation]) {
NSLog(#"here");
[self removeItemsFromView];
_headerNode.portLabel.text = [IPGameManager sharedGameData].world.player.port.name;
[_portNode show];
}
SKNode *nodeInPort = [_portNode nodeAtPoint:[touch locationInNode:_portNode]];
if ([nodeInPort.name isEqualToString:#"marketButton"]) {
[self showMarket];
}
}
}
In the showMarket function, a 'Done' SKSpriteNode is added so the user can leave the market when he/she wants to. As you can see above, if the _tableDoneButton contains the touch location, it should call the removeItemsFromView function which contains [_tableDoneButton removeFromParent];. Do I have to do something special when removing nodes this way?
Another issue: It seems to get removed ~1 second later. What exactly happens when you run [node removeFromParent]? If I set the SKSpriteNode to nil after I call [self removeItemsFromView] it works fine. But I shouldn't have to do that?
How can we get the time a user spent to finish a swipe right or swipe left action? Already googled but no luck. If there is not built-in solution already, will touch begin and touch end together work?
Measure the start and end time, and calculate the difference in the UIGestureRecognizer callback method for gestureRecognizer.state == UIGestureRecognizerStateBegan and gestureRecognizer.state == UIGestureRecognizerStateEnded
You can do this by getting the date (which you create as ivar or property) in the touchesBegan method like this:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
touchStartDate = [NSDate date];
}
Then you fetch the difference in the touches end method:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
NSTimeInterval *touchDuration = [[NSDate date] timeIntervalSinceDate:touchStartDate];
NSLog(#"Touchduration %f",touchDuration);
}
If you only want to get the duration of a swipe left/right gesture you also need to check if the x value of the touch changed. This should be easy.
I can't find anything to explain lost UITouch events. If you smash your full hand on the screen enough times, the number of touchesBegan will be different than the number of touchesEnded! I think the only way to actually know about these orphaned touches will be to reference them myself and keep track of how long they haven't moved.
Sample code:
int touchesStarted = 0;
int touchesFinished = 0;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
touchesStarted += touches.count;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
touchesFinished += touches.count;
NSLog(#"%d / %d", touchesStarted, touchesFinished);
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[self touchesEnded:touches withEvent:event];
}
Don't forget about touchesCancelled: UIResponder reference
Editing in response to the poster's update:
Each touch object provides what phase it is in:
typedef enum {
UITouchPhaseBegan,
UITouchPhaseMoved,
UITouchPhaseStationary,
UITouchPhaseEnded,
UITouchPhaseCancelled,
} UITouchPhase;
I believe that if a touch starts and ends in the same touch event set, -touchesBegan:withEvent: will be called but will contain touches which have ended or cancelled.
You should change your counting code, then, to look like this:
int touchesStarted = 0;
int touchesFinished = 0;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self customTouchHandler:touches];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self customTouchHandler:touches];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[self customTouchHandler:touches];
}
- (void)customTouchHandler:(NSSet *)touches
{
for(UITouch* touch in touches){
if(touch.phase == UITouchPhaseBegan)
touchesStarted++;
if(touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled)
touchesFinished++;
}
NSLog(#"%d / %d", touchesStarted, touchesFinished);
}
Every single touch event will go through both phases of started and finished/cancelled, and so your counts should match up as soon as your fingers are off the screen.
One thing to remember... You may receive some touches that have multiple taps. Don't forget to take tapCount into account.
However, if you still have the problem, you can consider all touches from the event, though it presents some other management issues...
HACK ALERT
I have coded the following HACK to work around this. Sometimes the touchesEnded does not get called, BUT, the touches show up as part of all the touches in the event.
Note, that you can now process the same "canceled" or "ended" touch multiple times. If that is a problem, you have to keep your own state of "pending" touches, and remove them when done.
Yeah, it all is pretty bad, but I don't know how to overcome this issue without a similar hack. The basic solution is to look at all the touches in each event, and process them based on their phase, calling the appropriate ended/canceled when they are seen.
- (void) touchesEndedOrCancelled:(NSSet *)touches
{
__block NSMutableSet *ended = nil;
__block NSMutableSet *canceled = nil;
[touches enumerateObjectsUsingBlock:^(UITouch *touch, BOOL *stop) {
if (touch.phase == UITouchPhaseEnded) {
if (!ended) ended = [NSSet setWithObject:touch];
else [ended addObject:touch];
} else if (touch.phase == UITouchPhaseCancelled) {
if (!canceled) canceled = [NSSet setWithObject:touch];
else [canceled addObject:touch];
}
}];
if (ended) [self touchesEnded:ended withEvent:nil];
if (canceled) [self touchesCancelled:canceled withEvent:nil];
}
Then, call it at the end of touchesBegan and touchesMoved...
[self touchesEndedOrCancelled:event.allTouches];
For this to work, touchesEnded/Canceled needs to not choke on a nil event. Also, the "other" needs to be handled. In touchesEnded...
[self touchesCancelled:[event.allTouches objectsPassingTest:^BOOL(UITouch *touch, BOOL *stop) {
return touch.phase == UITouchPhaseCancelled;
}] withEvent:nil];
and in touchesCanceled...
[self touchesEnded:[event.allTouches objectsPassingTest:^BOOL(UITouch *touch, BOOL *stop) {
return touch.phase == UITouchPhaseEnded;
}] withEvent:nil];
You may need to provide more detail but....
If you run your finger off the edge of the screen this event will show touch started and touch moved but no end as it didn't actually end (lift finger). This could be your problem, that the event didn't happen. Plus there is a limit to the amount of touches in multi touch if you press say 10 times its then up to the system to determine what events are really and what are false, it seems like you are using the hardware incorrectly.
In my game I need to calculate duration of touch. I did this by :
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
self.endTime = [NSDate date]; //NSDate *endTime in .h
NSLog(#"%#",self.endTime);
}
-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
tStart = [[NSDate date] timeIntervalSinceDate:self.endTime];
NSLog(#"duration %f",tStart);
I am using this time interval as a factor to calculate height of the jump the player makes.
Less is the tStart, low is the jump and more is the tStart, high is the jump. I am doing this as :
if(tStart/1000<=9.430)
{
[player jump:5.0f];
}
else if(tStart>9.430 && tStart<=9.470)
{
[player jump:7.0f];
}
else if(tStart/1000>9.470)
{
[player jump:8.0f];
}
However I want to perform this action on tochBegan so that player may jump as soon as screen is touched. For this need the value of tStart in touchBegan. How should I do that?
Thanks
For a given touch, the UITouch instance is the same, so at ccTouchBegan save the touch/touches with the oldest timestamp and then wait for ccTouchEnded. When you get the UITouch that you previously saved, it means the player lifted the finger.
update
You can
Jump as soon as the user touches the screen.
Jump and boost the jump while the touch is ongoing up to x milliseconds (suggestion from grapefrukt).
Jump when the user releases the touch or a max time of x milliseconds has elapsed.
Jump and boost the jump until the touch is over.
Option 4 is unpractical because the user can keep pressing as long as he wants. So given that you want to make a variable jump, here is code for option 3:
UITouch *jump = nil;
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
// no jump ongoing, start one
if (jump==nil){
jump = touch;
// jump on a separate thread
[NSThread performSelectorInBackground:(SEL)jump withObject:touch];
}
}
-(void) jump:(UITouch*) touch {
NSInterval time = [touch timestamp]; // I *think* this is the time since system uptime :P
int jumpSpeed = 1;
// increase the jump speed for every 100ms up to 'maxTimeYouAreAllowedToJump' ms
while ( ([NSProcessInfo systemUptime]-time < maxTimeYouAreAllowedToJump) && (jump!=nil) ){
jumpSpeed += 1;
[NSThread sleepForTimeInterval:.1]; // sleep for 100ms
}
[self doTheJumpingWithSpeed:jumpSpeed]; // do the actual jump!
}
-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
// user released the touch that initiated the jump
if ((touch!=nil) && (touch==jump)) {
jump =nil;
}
}
I have a quick question regarding tracking touches on the iPhone and I seem to not be able to come to a conclusion on this, so any suggestions / ideas are greatly appreciated:
I want to be able to track and identify touches on the iphone, ie. basically every touch has a starting position and a current/moved position. Touches are stored in a std::vector and they shall be removed from the container, once they ended. Their position shall be updated once they move, but I still want to keep track of where they initially started (gesture recognition).
I am getting the touches from [event allTouches], thing is, the NSSet is unsorted and I seem not to be able to identify the touches that are already stored in the std::vector and refer to the touches in the NSSet (so I know which ones ended and shall be removed, or have been moved, etc.)
Here is my code, which works perfectly with only one finger on the touch screen, of course, but with more than one, I do get unpredictable results...
- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
[self handleTouches:[event allTouches]];
}
- (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
[self handleTouches:[event allTouches]];
}
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
[self handleTouches:[event allTouches]];
}
- (void) touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event
{
[self handleTouches:[event allTouches]];
}
- (void) handleTouches:(NSSet*)allTouches
{
for(int i = 0; i < (int)[allTouches count]; ++i)
{
UITouch* touch = [[allTouches allObjects] objectAtIndex:i];
NSTimeInterval timestamp = [touch timestamp];
CGPoint currentLocation = [touch locationInView:self];
CGPoint previousLocation = [touch previousLocationInView:self];
if([touch phase] == UITouchPhaseBegan)
{
Finger finger;
finger.start.x = currentLocation.x;
finger.start.y = currentLocation.y;
finger.end = finger.start;
finger.hasMoved = false;
finger.hasEnded = false;
touchScreen->AddFinger(finger);
}
else if([touch phase] == UITouchPhaseEnded || [touch phase] == UITouchPhaseCancelled)
{
Finger& finger = touchScreen->GetFingerHandle(i);
finger.hasEnded = true;
}
else if([touch phase] == UITouchPhaseMoved)
{
Finger& finger = touchScreen->GetFingerHandle(i);
finger.end.x = currentLocation.x;
finger.end.y = currentLocation.y;
finger.hasMoved = true;
}
}
touchScreen->RemoveEnded();
}
Thanks!
It appears the "proper" way to track multiple touches is by the pointer value of the UITouch event.
You can find more details in the "Handling a Complex Multi-Touch Sequence" section of this
Apple Developer Documentation
To fix your problem scrap your "handleTouches" method. The first thing you do in your handleTouches method, is switch it on the touchPhase, but that is already given to you. If you recieve the touch in touchesBegan, you know the touch is in UITouchPhaseBegan. By funneling touches from the four touch methods into one method, you are defeating the purpose of having four delegate methods.
In each of those methods, Apple gives you an opportunity to deal with a different phase of the current touch.
The second thing is that you don't need to search the event for the current touch, it is given to you as a parameter: touches.
An event is comprised of sets of touches. For convienence, you are given the current touches even though it can also be found within event.
So, in touchesBegan, you start tracking a touch.
- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event{
NSString *startPoint = NSStringFromCGPoint([[touches anyObject] locationInView:self]);
NSDictionary * touchData = [NSDictionary dictionaryWithObjectsandKeys: startPoint, #"location", touches, #"touch"]
[startingLocations addObject:touchData];
}
I'm using an array of dictionaries to hold my touch data.
Try to seperate your code and move it into the appropriate touch method. For direction, Apple has a couple sample projects that focus on touches and show you how to setup those methods.
Remember, these methods will get called automatically for each touch during each phase, you don't need to cycle through the event to find out what happened.
The pointer to each set of touches remains constant, just the data changes.
Also, I would read the iPhone OS programming guide section on event handling which goes into greater depth of what I said above with several diagrams explaining the relationship of touches to events over time.
An excerpt:
In iPhone OS, a UITouch object
represents a touch, and a UIEvent
object represents an event. An event
object contains all touch objects for
the current multi-touch sequence and
can provide touch objects specific to
a view or window (see Figure 3-2). A
touch object is persistent for a given
finger during a sequence, and UIKit
mutates it as it tracks the finger
throughout it. The touch attributes
that change are the phase of the
touch, its location in a view, its
previous location, and its timestamp.
Event-handling code evaluates these
attributes to determine how to respond
to the event.
You should be able to properly collate your touches by storing the previous location of all touches and then comparing these previous locations when new touches are detected.
In your -handleTouches method, you could put something like this in your for loop:
// ..existing code..
CGPoint previousLocation = [touch previousLocationInView:self];
// Loop through previous touches
for (int j = 0; j < [previousTouchLocationArray count]; j++) {
if (previousLocation == [previousTouchLocationArray objectAtIndex:j]) {
// Current touch matches - retrieve from finger handle j and update position
}
}
// If touch was not found, create a new Finger and associated entry
Obviously you'll need to do some work to integrate this into your code, but I'm pretty sure you can use this idea to correctly identify touches as they move around the screen. Also I just realized CGPoint won't fit nicely into an NSArray - you'll need to wrap these in NSValue objects (or use a different type of array).