I have view where pan a view from one location to another. I can get the location of the points when moved, but I want to do some action when pan has not moved or is idle in certain point. I Don't see any state to show pan gesture is idle. The touch event I came across is UITouchPhaseStationary nut I don't know how to implement it.
There isn't a state for that, and UITouchPhaseStationary as explained in this post is for multi-touch, and lets you know if there is a second stationary finger on the screen while another finger is moving.
However, you can implement something like this yourself. Just make a timer with a time interval set to the timeout before the touch should be considered stationary and run it when the gesture's position changes. You'll want to invalidate the timer when the gesture ends as well as when the gesture changes to reset the timer. Here's an example.
- (void)gestureDidFire:(UIPanGestureRecognizer *)gesture
{
static NSTimer *timer = nil;
if (gesture.state == UIGestureRecognizerStateChanged) {
if (timer.isValid) {
[timer invalidate];
}
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(timerDidFire:) userInfo:nil repeats:NO];
}else if (gesture.state == UIGestureRecognizerStateEnded) {
if (timer.isValid) {
[timer invalidate];
}
}
}
- (void)timerDidFire:(NSTimer *)timer
{
NSLog(#"Stationary");
}
You could implement touchesEnded:withEvent: to determine when touch has finished. Set a flag on your panGesture method and then check that value
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.hasMoved) {
// Do something
self.hasMoved = NO;
}
}
Try this
- (void)panRecognized:(UIPanGestureRecognizer *)rec
{
CGPoint vel = [rec velocityInView:self.view];
if (vel.x == 0 && vel.y == 0)
{
// Not moved
}
else
// moved
}
Related
I want to add logout functionality in my app, so that user gets logout on inactivity/idle time since last screen touch.
Please suggest me the way to implement this functionality.
Some people are telling add this in your code;
- (void)sendEvent:(UIEvent *)event {
[super sendEvent:event];
// Only want to reset the timer on a Began touch or an Ended touch, to reduce the number of timer resets.
NSSet *allTouches = [event allTouches];
if ([allTouches count] > 0) {
// allTouches count only ever seems to be 1, so anyObject works here.
UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;
if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded)
[self resetIdleTimer];
}
}
- (void)resetIdleTimer {
if (idleTimer) {
[idleTimer invalidate];
[idleTimer release];
}
idleTimer = [[NSTimer scheduledTimerWithTimeInterval:maxIdleTime target:self selector:#selector(idleTimerExceeded) userInfo:nil repeats:NO] retain];
}
- (void)idleTimerExceeded {
NSLog(#"idle time exceeded");
}
But my question is where to add this code.
Get rid of all of your timer code and simply do this. When the user taps, schedule your idle method to be called after X seconds (using performSelector: afterDelay:). Anytime they tap, cancel all scheduled requests (using cancelPreviousPerformRequestsWithTarget:) and request a new one for X seconds.
int secondsUntilTimeout = 120;//time you want until they time-out.
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(idleTimerExceeded) object:nil];//cancel all previously scheduled time-out requests
[self performSelector:#selector(idleTimerExceeded) withObject:nil afterDelay:secondsUntilTimeout];//schedule a new time-out request
So your final code will look like this:
- (void)sendEvent:(UIEvent *)event {
[super sendEvent:event];
NSSet *allTouches = [event allTouches];
if ([allTouches count] > 0) {
UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;
if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded) {
int secondsUntilTimeout = 120;//time you want until they time-out.
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(idleTimerExceeded) object:nil];//cancel all previously scheduled time-out requests
[self performSelector:#selector(idleTimerExceeded) withObject:nil afterDelay:secondsUntilTimeout];//schedule a new time-out request
}
}
}
- (void)idleTimerExceeded {
NSLog(#"idle time exceeded");
}
Here is the swift version of code snippet which detects idle time on screen and can fire events such as auto logout/API call/other event call. Go to following simple steps
Initialise following three lines of code to setup duration and schedule time
var secondsUntilTimeout : Int = 60 //Duration(in seconds) after you want to fire and event
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(callPrintEventAfterTimeExceeded), object: nil) //Cancel if any previous time out request is running
perform(#selector(callPrintEventAfterTimeExceeded), with: nil, afterDelay: TimeInterval(secondsUntilTimeout)) //Set a new time out request to fire an event
Write desired code of any event in following method. You can write your own method name according to the requirement
#objc func callPrintEventAfterTimeExceeded() {
print("idle time exceeded")
}
I'm trying to make a sort of free runner game. I make the character jump by setting an integer to 30 or so every time the screen is tapped, and moving the character up the screen using CGPointMake(). Then I have a timer start when the player taps the screen which pulls the character down until he's reached a certain point (y=260). Then the timer invalidates and the character's downward movement stops.
This goes on and on and works perfectly well until you tap on the screen before the character reaches y=260. This makes him jerk up and come speeding down very fast and he doesn't stop at y=260 like he should. Here's the code that I think is causing the problem:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
playerMoving = 33;
gravity = [NSTimer scheduledTimerWithTimeInterval:.2
target:self
selector:#selector(playerJump)
userInfo:nil
repeats:YES];
}
Every time I double tap on the screen the timer gets activated again, even though it is already activated, and this is what I think is causing the problem.
Is there any way to not let the player tap on the screen a second time until the character has reached y=260 (so basically the player won't be able to double jump)?
Don't create the timer if it already exists. Nil out the variable when you're done with the timer, and check it before you create it again.
- (void)playerJump:(NSTimer *)tim
{
// Do stuff...
if( /* Done repeating */ ){
[gravity invalidate];
gravity = nil;
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
playerMoving = 33;
if( !gravity ){
gravity = [NSTimer scheduledTimerWithTimeInterval:.2
target:self
selector:#selector(playerJump:)
userInfo:nil
repeats:YES];
}
}
I have a view to which I've added both a pan and long press UIGestureRecognizer. The pan is used to move the view around. What I'd like to do is notice also that the touch has stopped moving (while remaining active) and trigger the long press.
What I find is that the long press is never triggered after the pan has begun. I've tried setting a delegate and implementing:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
NSLog(#"simultaneous %#", gestureRecognizer.class);
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
NSLog(#"require fail %#", gestureRecognizer.class);
return [gestureRecognizer isKindOfClass:[UIPanGestureRecognizer self]];
// also tried return YES;
// also tried return [gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer self]];
}
I've tried fooling with the pan gr's allowableMovement, also to no avail. I'm just about to give up and use a timer in the pan gr that get's invalidated and then reset on moves, but I was hoping that the SDK would do the state machine stuff for me.
In case anyone else needs it, here's the code that works for me. The goal is to have a view that is sensitive to both long press and pan, including a long press that isn't preceded by a pan and vice versa.
// setup
#property (strong,nonatomic) NSTimer *timer; // triggers the long press during pan
#property (strong,nonatomic) UIView *longPressView; // need this to track long press state
// view is the view we're interested in panning and long pressing
UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panGR:)];
[view addGestureRecognizer:panGR];
// this starts a long press when no pan has occurred
UILongPressGestureRecognizer *longGR = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressGR:)];
[view addGestureRecognizer:longGR];
When a pan begins or changes, start a timer. If the timer expires before the pan ends (touch releases), then we have a long press.
- (void)panGR:(UIPanGestureRecognizer *)gr {
if (gr.state == UIGestureRecognizerStateBegan) {
[self startTimer:gr.view];
} else if (gr.state == UIGestureRecognizerStateChanged) {
[self startTimer:gr.view];
// do whatever you want to do with pan state in this method
// in my case, I'm translating the view here
} else if (gr.state == UIGestureRecognizerStateEnded) {
if (self.longPressView) {
[self longPressEnded];
} else {
[self.timer invalidate];
}
}
}
We give the timer user info of the view. You might need to store other parts of the gesture state, like location, etc. Do it the same way, with the user info dictionary.
- (void)startTimer:(UIView *)view {
if (self.longPressView) return;
[self.timer invalidate];
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.8 target:self
selector:#selector(longPressTimer:)
userInfo:#{ #"view": view} repeats:NO];
}
-(void)longPressTimer:(NSTimer *)timer {
self.longPressView = timer.userInfo[#"view"];
[self longPressBegan];
}
Since the timer method won't have an associated gr, factor out all of the logic that we'd normally put in the gr handler so it can be called by both the timer handler and the gr handler.
- (void)longPressGR:(UILongPressGestureRecognizer *)gr {
if (gr.state == UIGestureRecognizerStateBegan) {
self.longPressView = gr.view;
[self longPressBegan];
} else if (gr.state == UIGestureRecognizerStateEnded) {
[self longPressEnded];
}
}
- (void)longPressBegan {
NSLog(#"long press began");
}
- (void)longPressEnded {
self.longPressView = nil;
NSLog(#"long press ended");
}
First of we have to register both long press and pag gesture events like,
let longPress = UILongPressGestureRecognizer()
longPress.delegate = self
longPress.addTarget(self, action: #selector(sendMessageLongPress(_:)))
let panGesture = UIPanGestureRecognizer()
panGesture.delegate = self
panGesture.addTarget(self, action: #selector(sendMessagePanGesture(_:)))
self.imgRecord.addGestureRecognizer(longPress)
self.imgRecord.addGestureRecognizer(panGesture)
then we have to setup to capture multiple touch events via delegate method. For this we have to extend UIGestureRecognizerDelegate and then use,
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
then we can implement events as we needed.
(in my case I wanted to cancel recording audio if user swipes, then I had to consider about touch started point and touch ended point as needed.)
Is there any way to get notified for continuous press on UIView? i.e. I keep pressing UIView with one finger and i want to keep calling particular method during that duration again and again.
I tried UILongPressGestureRecognizer but it just get notify of began, end, moved etc. Same as TouchesBegan.
Cheers
On TouchesBegan start a timer and perform a selector when you've reached a desired amount of time (long press).
On TouchesEnded invalidate the timer to prevent the selector from being performed.
I would also set up an extra flag detecting "fingerReleased":
Set fingerReleased = NO on TouchesBegan and fingerReleased = YES on TouchesEnded and put the code you want to execute in a:
if (!fingerReleased)
{
// Execute code
}
fire NSTimer in touchesBegan to call a selector that you want and invalidate it in touchesEnded method
What you can do is, use the combine functionality of UILongPressGestureRecognizer and NSTimer.
The following code should meet your requirement.
#property (strong, nonatomic) NSTimer *timer;
- (void) viewDidLoad
{
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
[aView addGestureRecognizer: longPress];
}
- (void) longPress:(UILongPressGestureRecognizer*)gesture
{
if ( gesture.state == UIGestureRecognizerStateBegan )
{
self.timer=[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:#selector(callMethod) userInfo:nil repeats:YES];
[self.timer fire];
}
if ( gesture.state == UIGestureRecognizerStateEnded )
{
[self.timer invalidate];
}
}
- (void) callMethod
{
//will be called continuously within certain time interval you have set
}
I know that it is possible to detect a touch on iOS using
UITouch *touch = [[event allTouches] anyObject];
However, is it possible to find out when a user does not touch?
EDIT
I want a method to be executed when the user does not touch the screen for 5 seconds. Is this possible?
I do not have any custom methods that react to touches. I only have the existing methods
-touchesBegan
-touchesMoved and
-touchesEnded
To be more specific, The user can keep touching the screen as many times as he wants, for how long ever he wants. But, when the user does not touch the screen for more than 5 seconds, then a method -sampleMethod needs to be fired.
You can start a timer with a 5 second interval and every time you get a touch, restart the timer:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.timer invalidate];
self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:#selector(yourMethod) userInfo:nil repeats:NO];
}
- (void)yourMethod {
NSLog(#"not touched for 5 seconds");
}
Depending on your specific needs, you might want to use touchesEnded:withEvent instead.
I'm going to take a whack at an answer here. Because in the comments you clarified what you're trying to do. Something after 5 seconds with no response. What I'm showing here is typically used in opengl apps which all my apps are. But something like it should work for you even if your not in open gl.
You need something that runs continuously...
- (void) startAnimation
{
if (!animating)
{
displayLink = [NSClassFromString(#"CADisplayLink") displayLinkWithTarget:self selector:#selector(drawView)];
[displayLink setFrameInterval:animationFrameInterval];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
animating = TRUE;
}
}
- (void)stopAnimation
{
if (animating)
{
[displayLink invalidate];
displayLink = nil;
animating = FALSE;
}
}
We use this in oepngl apps to run the drawview function every 60th of a second timed with the refresh of the display. I don't see why you couldn't do it. Then in you drawView method check the time at the beginning and take care of any other crap you need to, like advancing pieces in a game or just checking to see how long messages have been up..
- (void)drawView
{
timeThisRound = CFAbsoluteTimeGetCurrent();
And check it against whatever event triggers the start of the 5 seconds. If you're past 5 seconds then do whatever you're going to do instead of waiting any longer for them to tap the button.
I have my own messaging system that does this. I can set for any message that comes up if it should go away on it's own after 5 seconds. Or if they tap it, it goes away faster. I use the timeThisRound method (a global property) everywhere to track when NOW is so that I can have timing based things as well as touch based things.
Sure, the rest of the time. What do you mean? The only way would be to set a boolean flag to false, and in your "touched" method, set it to true. Then anytime it's false, there's been no touch...
Launch a method after a certain delay, starting when user stops touching the view.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self performSelector:#selector(sampleMethod) withObject:nil afterDelay:5.0f];
}
If user touches the view again, you should cancel the pending method call
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(sampleMethod) object:nil];
}
Remember to put a cancel of the pending method call in dealloc too
- (void)dealloc {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(sampleMethod) object:nil];
}