iPhone: Detecting user inactivity/idle time since last screen touch
Has anybody implemented a feature where if the user has not touched the screen for a certain time period, you take a certain action? I'm trying to figure out the best way to do that.
There's this somewhat-related method in UIApplication:
[UIApplication sharedApplication].idleTimerDisabled;
It'd be nice if you instead had something like this:
NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;
Then I could set up a timer and periodically check this value, and take some action when it exceeds a threshold.
Hopefully that explains what I'm looking for. Has anyone tackled this issue already, or have any thoughts on how you would do it? Thanks.
Solution 1:
Here's the answer I had been looking for:
Have your application delegate subclass UIApplication. In the implementation file, override the sendEvent: method like so:
- (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");
}
where maxIdleTime and idleTimer are instance variables.
In order for this to work, you also need to modify your main.m to tell UIApplicationMain to use your delegate class (in this example, AppDelegate) as the principal class:
int retVal = UIApplicationMain(argc, argv, @"AppDelegate", @"AppDelegate");
Solution 2:
I have a variation of the idle timer solution which doesn't require subclassing UIApplication. It works on a specific UIViewController subclass, so is useful if you only have one view controller (like an interactive app or game may have) or only want to handle idle timeout in a specific view controller.
It also does not re-create the NSTimer object every time the idle timer is reset. It only creates a new one if the timer fires.
Your code can call resetIdleTimer
for any other events that may need to invalidate the idle timer (such as significant accelerometer input).
@interface MainViewController : UIViewController
{
NSTimer *idleTimer;
}
@end
#define kMaxIdleTimeSeconds 60.0
@implementation MainViewController
#pragma mark -
#pragma mark Handling idle timeout
- (void)resetIdleTimer {
if (!idleTimer) {
idleTimer = [[NSTimer scheduledTimerWithTimeInterval:kMaxIdleTimeSeconds
target:self
selector:@selector(idleTimerExceeded)
userInfo:nil
repeats:NO] retain];
}
else {
if (fabs([idleTimer.fireDate timeIntervalSinceNow]) < kMaxIdleTimeSeconds-1.0) {
[idleTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:kMaxIdleTimeSeconds]];
}
}
}
- (void)idleTimerExceeded {
[idleTimer release]; idleTimer = nil;
[self startScreenSaverOrSomethingInteresting];
[self resetIdleTimer];
}
- (UIResponder *)nextResponder {
[self resetIdleTimer];
return [super nextResponder];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self resetIdleTimer];
}
@end
(memory cleanup code excluded for brevity.)
Solution 3:
For swift v 3.1
dont't forget comment this line in AppDelegate //@UIApplicationMain
extension NSNotification.Name {
public static let TimeOutUserInteraction: NSNotification.Name = NSNotification.Name(rawValue: "TimeOutUserInteraction")
}
class InterractionUIApplication: UIApplication {
static let ApplicationDidTimoutNotification = "AppTimout"
// The timeout in seconds for when to fire the idle timer.
let timeoutInSeconds: TimeInterval = 15 * 60
var idleTimer: Timer?
// Listen for any touch. If the screen receives a touch, the timer is reset.
override func sendEvent(_ event: UIEvent) {
super.sendEvent(event)
if idleTimer != nil {
self.resetIdleTimer()
}
if let touches = event.allTouches {
for touch in touches {
if touch.phase == UITouchPhase.began {
self.resetIdleTimer()
}
}
}
}
// Resent the timer because there was user interaction.
func resetIdleTimer() {
if let idleTimer = idleTimer {
idleTimer.invalidate()
}
idleTimer = Timer.scheduledTimer(timeInterval: timeoutInSeconds, target: self, selector: #selector(self.idleTimerExceeded), userInfo: nil, repeats: false)
}
// If the timer reaches the limit as defined in timeoutInSeconds, post this notification.
func idleTimerExceeded() {
NotificationCenter.default.post(name:Notification.Name.TimeOutUserInteraction, object: nil)
}
}
create main.swif file and add this (name is important)
CommandLine.unsafeArgv.withMemoryRebound(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)) {argv in
_ = UIApplicationMain(CommandLine.argc, argv, NSStringFromClass(InterractionUIApplication.self), NSStringFromClass(AppDelegate.self))
}
Observing notification in an any other class
NotificationCenter.default.addObserver(self, selector: #selector(someFuncitonName), name: Notification.Name.TimeOutUserInteraction, object: nil)
Solution 4:
This thread was a great help, and I wrapped it up into a UIWindow subclass that sends out notifications. I chose notifications to make it a real loose coupling, but you can add a delegate easily enough.
Here's the gist:
http://gist.github.com/365998
Also, the reason for the UIApplication subclass issue is that the NIB is setup to then create 2 UIApplication objects since it contains the application and the delegate. UIWindow subclass works great though.
Solution 5:
Actually the subclassing idea works great. Just don't make your delegate the UIApplication
subclass. Create another file that inherits from UIApplication
(e.g. myApp). In IB set the class of the fileOwner
object to myApp
and in myApp.m implement the sendEvent
method as above. In main.m do:
int retVal = UIApplicationMain(argc,argv,@"myApp.m",@"myApp.m")
et voilà!