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.
Related
I'm writing a radial menu, where when you long press (UILongPressGestureRecognizer) on the screen, it pops out a menu of buttons, and I can drag my finger (which is already touching the screen) over one of the buttons, which selects, then when I let go, it performs an action specific to that button.
I currently have the radial menu as a UIControl subclass, and I'm trying to override beginTrackingWithTouch: and continueTrackingWithTouch:, but the long press that shows the menu (adds it to the superview), does not get transferred to a touch recognized by the UIControl.
Any ideas how I can "forward" this touch event from the UIControl's superview to it?
Thanks!
Not a direct answer, but you should really watch the WWDC session about scrollviews of this year. And then watch it again. It contains a fantastic amount of information, and most certainly an answer to your question. It is session 235: advanced scrollviews and touch handling techniques.
I would do this...
The long press handler:
-(IBAction)onLongPress:(UILongPressGestureRecognizer*)recognizer
{
CGPoint point = [recognizer locationInView:self.view];
if (recognizer.state == UIGestureRecognizerStateBegan) {
//create the radial view and add it to the view
CGSize radialViewSize = CGSizeMake(80, 80);
radialView = [[RadialView alloc] initWithFrame:CGRectMake(point.x - radialViewSize.width/2, point.y - radialViewSize.height/2, radialViewSize.width, radialViewSize.height)];
[self.view addSubview:radialView];
radialView.backgroundColor = [UIColor redColor];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
[radialView onTouchUp:[radialView convertPoint:point fromView:self.view]];
[radialView removeFromSuperview];
radialView = nil;
}
}
In your radial view: (I suppose that the radial view keeps the buttons in an array)
-(void)onTouchUp:(CGPoint)point
{
for (UIButton *button in buttons) {
if ([button pointInside:[self convertPoint:point toView:button] withEvent:nil]) {
//This button got clicked
//send button clicked event
[button sendActionsForControlEvents:UIControlEventTouchUpInside];
}
}
}
I know it's not perfect, since the touch events don't get forwarded to the radial view (as you asked), but it let's you click the buttons. Hope it helps!
I'm not sure if this is the same behavior you're looking for, but I recently had to overcome the exact same issue when developing my Concentric Radial Menu. The thing I discovered very quickly was that views added to the view hierarchy during the touch event do not get re-hit-tested and therefore seem unresponsive until the next event comes around.
The solution I used, which I can't say I love, was to implement a custom UIWindow subclass that intercepts - (void)sendEvent:(UIEvent *)event and forwards those events to the "active" radial menu.
That is, the menu registers with the window upon activation, then unregisters when being unloaded. If done atomically, this is actually a pretty safe technique, I just wish it were cleaner than it is.
Best of luck!
I need two functions which would be fired when the rotation gesture starts and finishes, because I need to know the whole angle of the rotation. Currently the gesture recogniser is fired all the time until the rotation finishes, and I cannot find out when it has finished, to find to total angle.
That's because the method you hook to your gesture gets called for all of the gestures states, like began/ended/canceled/changed. You can however ask the gesture for its current state within the method, and add specific functionality for these different states. Here's a basic example:
- (void)rotationGestureHandler:(UIRotationGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan) {
// do stuff - call method for gesture began
}else if (gesture.state == UIGestureRecognizerStateEnded) {
// do other stuff - call method for gesture ended
}
}
I have a view with four pan gestures attached. The first has both max and min number of touches set to 1, the second to 2, etc. This makes it so each will only recognize one touch while up to four fingers slide around on the screen.
That's working dandy. What isn't working is detecting when individual touches end. Anything I have set to happen when a gesture ends only happens when all gestures have ended completely.
Example delegate method:
- (void) handlePan:(UIPanGestureRecognizer*)recognizer {
//Setting what happens when a gesture is recognized as beginning
if (recognizer.state == UIGestureRecognizerStateBegan) {
//...whatever happens, bunnies follow your finger or whatever
} else
//Setting what happens when a gesture ends
if ((recognizer.state == UIGestureRecognizerStateEnded) |
(recognizer.state == UIGestureRecognizerStateCancelled) |
(recognizer.state == UIGestureRecognizerStateFailed)) {
NSLog(#"end");
}
}
What should be happening is that I see "end" in the console whenever any finger is lifted. Instead, I see nothing until all fingers are lifted, at which point I see "end" repeated four times (or as many times as fingers that were on the screen).
Is there any way I can make this work the way I intend?
edit After fiddling I see that I may not be analyzing my problem correctly. The whole reason I want to detect when a gesture's touch ends is that I want to have gestures able to become active when there is more than one touch on screen, but I want each gesture to only track one touch itself. I was setting an "active" flag on gestures that were tracking touches, and then toggling that flag off after touches ended, and that wasn't working, because touch-end-detection was hard to implement well.
But if there's a different way to achieve the same thing, that's the real thing I'm looking for: among many overlapping touches, have each gesture recognizer track one and only one.
You may want to do something like - it catches the change in fingers on the screen for the given gesture; you may need to add some more logic surrounding which gesture you're working with:
switch( recognizer.numberOfTouches ) {
case 1: {
NSLog(#"1 ");
break;
}
case 2: {
NSLog(#"2");
break;
}
case 3: {
NSLog(#"3");
break;
}
case 4: {
NSLog(#"4");
break;
}
default: {
NSLog(#"0");
}
}
This is what eventually worked.
In short, I made a flag that flipped whenever a gesture recognizer was assigned a touch, ensuring no other recognizers accepted that touch. I also tested each recognizer to make sure it only accepted a touch when it wasn't already following a touch. So I made each touch only get assigned once, and each recognizer only accept one touch. Worked like a charm.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//set this to no every time a new touch happens, meaning it isn't taken yet.
touchTaken = NO;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
//If the touch is taken or the gesture's already following a touch, say no.
if (touchTaken | ([gestureRecognizer numberOfTouches] > 0)) {
return NO;
}
else {
touchTaken = YES;
return YES;
}
}
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 have the following problem.
I am using a UILongPressGestureRecognizer to put a UIView into a "toggle mode". If the UIView is in "toggle mode" the user is able to drag the UIView around the screen. For dragging the UIView around the screen I am using the methods touchesBegan, touchesMoved and touchesEnded.
It works, but: I have to lift my finger in order to drag it, because the touchesBegan method got already called and therefore is not called again and therefore I can't drag the UIView around the screen.
Is there any way to manually call touchesBegan after UILongPressGestureRecognizer got triggered (UILongPressGestureRecognizer changes a BOOL value and the touchesBegan only works if this BOOL is set to YES).
UILongPressGestureRecognizer is a continuous gesture recognizer, so rather than resorting to touchesMoved or UIPanGestureRecognizer, just check for UIGestureRecognizerStateChanged, e.g.:
- (void)viewDidLoad
{
[super viewDidLoad];
UILongPressGestureRecognizer *gesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
[self.view addGestureRecognizer:gesture];
}
- (void)handleGesture:(UILongPressGestureRecognizer *)gesture
{
CGPoint location = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan)
{
// user held down their finger on the screen
// gesture started, entering the "toggle mode"
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
// user did not lift finger, but now proceeded to move finger
// do here whatever you wanted to do in the touchesMoved
}
else if (gesture.state == UIGestureRecognizerStateEnded)
{
// user lifted their finger
// all done, leaving the "toggle mode"
}
}
I would suggest you to use UIPanGestureRecognizer as it a recommended gesture for dragging.
You can configure the min. and max. number of touches required for a panning, using the following the properties:
maximumNumberOfTouches
minimumNumberOfTouches
You can handle the states like Began, Changed and Ended, like having animation for the required states.
Using the below method translate the point to the UIView in which you want it.
- (void)setTranslation:(CGPoint)translation inView:(UIView *)view
example:
You have to use a global variable to retain the old frame. Get this in UIGestureRecognizerStateBegan.
When the state is UIGestureRecognizerStateChanged. You can use the
-(void) pannningMyView:(UIPanGestureRecognizer*) panGesture{
if(panGesture.state==UIGestureRecognizerStateBegan){
//retain the original state
}else if(panGesture.state==UIGestureRecognizerStateChanged){
CGPoint translatedPoint=[panGesture translationInView:self.view];
//here you manage to get your new drag points.
}
}
Velocity of the drag. Based on the velocity you can provide a animation to show bouncing of a UIView
- (CGPoint)velocityInView:(UIView *)view