158

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.

TheNeil
  • 3,321
  • 2
  • 27
  • 52
Mike McMaster
  • 7,573
  • 8
  • 37
  • 42
  • This is a great question. Windows has the concept of an OnIdle event but I think it's more about the app not currently handling anything in it's message pump vs the iOS idleTimerDisabled property which seems only concerned with locking the device. Anyone know if there's anything even remotely close to the Windows concept in iOS/MacOSX? – stonedauwg Mar 28 '16 at 14:17

10 Answers10

158

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");
Mike McMaster
  • 7,573
  • 8
  • 37
  • 42
  • 3
    Hi Mike, My AppDelegate is inherting from NSObject So changed it UIApplication and Implement above methods to detect user becoming idle but i am getting error "Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'There can only be one UIApplication instance.'".. is anything else i need to do...? – Mihir Mehta Jun 23 '10 at 06:42
  • 8
    I would add that the UIApplication subclass should be separate from the UIApplicationDelegate subclass – boliva Mar 15 '12 at 21:30
  • I am NOT sure how this will work with device entering the inactive state when timers stop firing? – anonmys Jun 19 '12 at 19:31
  • doesn't work properly if I assign using of popToRootViewController function for timeout event. It happens when I show UIAlertView, then popToRootViewController, then I press any button on UIAlertView with a selector from uiviewController which is already popped – Gargo Oct 24 '12 at 09:00
  • 4
    Very nice! However, this approach creates a lot of `NSTimer` instances if there are a lot of touches. – Andreas Ley Dec 03 '12 at 15:48
  • Have a look at my query http://stackoverflow.com/questions/20088521/calling-method-from-principal-class-in-objective-c . – Tirth Nov 20 '13 at 10:30
  • will it work if my app goes to suspended in background? what about NStimer will it work for long background time? – Jagdev Sendhav Jan 27 '14 at 06:56
  • Thanks. Very good solution. The solution mentioned by Chris Miles below also seems good(http://stackoverflow.com/a/5269900/309046). But, it might fail in some conditions. – Satish Aug 29 '14 at 06:47
  • I think your idleTimerExceeded should have signature to take an NSTimer object. - (void)idleTimerExceeded:(NSTimer *)timer {} – karim Jul 10 '15 at 08:00
90

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.)

Chris Miles
  • 7,346
  • 2
  • 37
  • 34
  • 1
    Very good. This answer rocks! Beats the answer marked as correct although i know it was a lot earlier but this is a better solution now. – Chintan Patel Jun 08 '11 at 19:48
  • This is great, but one issue I found: scrolling in UITableViews doesn't cause nextResponder to be called. I also tried tracking via touchesBegan: and touchesMoved:, but no improvement. Any ideas? – Greg Maletic Mar 21 '13 at 18:22
  • 3
    @GregMaletic : i had the same issue but finally i have added - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { NSLog(@"Will begin dragging"); } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { NSLog(@"Did Scroll"); [self resetIdleTimer]; } have u tried this ? – Akshay Aher Jul 01 '13 at 07:38
  • Thanks. This is still helpful. I ported it to Swift and it worked great. – Mark.ewd Oct 30 '14 at 20:01
  • you are a rockstar. kudos – Pras Jan 14 '16 at 23:28
  • This worked, and could adapt this to implement in AppDelegate file. Thanks – Rakshitha Muranga Rodrigo Jul 18 '21 at 19:46
  • Hi, I ran into a problem with an UIAlertController being displayed with another automated timer... Once the UIAlert is shown, time reset again... is there any way to omit this kind of thing and only count screen input actions... – Rakshitha Muranga Rodrigo Oct 22 '21 at 12:50
23

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)
Sergey Stadnik
  • 337
  • 2
  • 9
12

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.

Brian King
  • 2,834
  • 1
  • 27
  • 26
  • 1
    can you tell me how to use your code? i dont understand how to called it – R. Dewi Jun 15 '11 at 09:59
  • 2
    It works great for touches, but doesn't seem to handle input from keyboard. This means it will time out if the user is typing stuff on the gui keyboard. – Martin Wickman Jul 22 '11 at 10:44
  • 2
    Me too not able to understand how to use it ...I add observers in my view controller and expecting notifications to b fired when app is untouched/idle .. but nothing happened ... plus from where we can control the idle time? like I want idle time of 120 seconds so that after 120 seconds IdleNotification should fire, not before that. – Ans Dec 21 '13 at 10:32
6

There's a way to do this app wide without individual controllers having to do anything. Just add a gesture recognizer that doesn't cancel touches. This way, all touches will be tracked for the timer, and other touches and gestures aren't affected at all so no one else has to know about it.

fileprivate var timer ... //timer logic here

@objc public class CatchAllGesture : UIGestureRecognizer {
    override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesBegan(touches, with: event)
    }
    override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        //reset your timer here
        state = .failed
        super.touchesEnded(touches, with: event)
    }
    override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesMoved(touches, with: event)
    }
}

@objc extension YOURAPPAppDelegate {

    func addGesture () {
        let aGesture = CatchAllGesture(target: nil, action: nil)
        aGesture.cancelsTouchesInView = false
        self.window.addGestureRecognizer(aGesture)
    }
}

In your app delegate's did finish launch method, just call addGesture and you're all set. All touches will go through the CatchAllGesture's methods without it preventing the functionality of others.

Jlam
  • 1,932
  • 1
  • 21
  • 26
  • 1
    I like this approach, used it for a similar issue with Xamarin: https://stackoverflow.com/a/51727021/250164 – Wolfgang Schreurs Aug 07 '18 at 16:45
  • 1
    Works great, also seems like this technique is used to control visibility of UI controls in AVPlayerViewController ([private API ref](https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/AVKit.framework/AVUserInteractionObserverGestureRecognizer.h)). Overriding app `-sendEvent:` is overkill, and `UITrackingRunLoopMode` does not handle many cases. – Roman B. Dec 16 '18 at 03:14
  • @RomanB. yeah exactly. when you've worked with iOS long enough you know to always use "the right way" and this is the straightforward way to implement a custom gesture as intended https://developer.apple.com/documentation/uikit/uigesturerecognizer/1620009-touchesbegan – Jlam Dec 17 '18 at 23:52
  • What does setting the state to .failed accomplish in touchesEnded? – stonedauwg May 08 '19 at 18:48
  • I like this approach but when tried it only seems to catch taps, not any other gesture like pan, swipe etc in touchesEnded where the reset logic would go. Was that intended? – stonedauwg May 08 '19 at 19:08
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à!

zx485
  • 28,498
  • 28
  • 50
  • 59
Roby
  • 51
  • 1
  • 1
  • 1
    Yep, creating a stand-alone UIApplication subclass seems to work fine. I left the second parm nil in main. – Hot Licks May 09 '11 at 21:53
  • @Roby, have a look that my query http://stackoverflow.com/questions/20088521/calling-method-from-principal-class-in-objective-c . – Tirth Nov 20 '13 at 10:28
4

I just ran into this problem with a game that is controlled by motions i.e. has screen lock disabled but should enable it again when in menu mode. Instead of a timer I encapsulated all calls to setIdleTimerDisabled within a small class providing the following methods:

- (void) enableIdleTimerDelayed {
    [self performSelector:@selector (enableIdleTimer) withObject:nil afterDelay:60];
}

- (void) enableIdleTimer {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    [[UIApplication sharedApplication] setIdleTimerDisabled:NO];
}

- (void) disableIdleTimer {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    [[UIApplication sharedApplication] setIdleTimerDisabled:YES];
}

disableIdleTimer deactivates idle timer, enableIdleTimerDelayed when entering the menu or whatever should run with idle timer active and enableIdleTimer is called from your AppDelegate's applicationWillResignActive method to ensure all your changes are reset properly to the system default behaviour.
I wrote an article and provided the code for the singleton class IdleTimerManager Idle Timer Handling in iPhone Games

Kay
  • 12,918
  • 4
  • 55
  • 77
4

Here is another way to detect activity:

The timer is added in UITrackingRunLoopMode, so it can only fire if there is UITracking activity. It also has the nice advantage of not spamming you for all touch events, thus informing if there was activity in the last ACTIVITY_DETECT_TIMER_RESOLUTION seconds. I named the selector keepAlive as it seems an appropriate use case for this. You can of course do whatever you desire with the information that there was activity recently.

_touchesTimer = [NSTimer timerWithTimeInterval:ACTIVITY_DETECT_TIMER_RESOLUTION
                                        target:self
                                      selector:@selector(keepAlive)
                                      userInfo:nil
                                       repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_touchesTimer forMode:UITrackingRunLoopMode];
Jasper
  • 7,031
  • 3
  • 35
  • 43
Mihai Timar
  • 1,141
  • 13
  • 21
  • how so? i do believe it's clear you should make you "keepAlive" selector yourself to whatever needs you have. Maybe i'm missing your point of view? – Mihai Timar Jun 09 '15 at 09:29
  • You say that this is another way to detect activity, however, this only instantiates an iVar which is a NSTimer. I don't see how this answers the OP's question. – Jasper Jun 09 '15 at 11:47
  • 1
    The timer is added in UITrackingRunLoopMode, so it can only fire if there is UITracking activity. It also has the nice advantage of not spamming you for all touch events, thus informing if there was activity in the last ACTIVITY_DETECT_TIMER_RESOLUTION seconds. I named the selector keepAlive as it seems an appropriate use case for this. You can of course do whatever you desire with the information that there was activity recently. – Mihai Timar Jun 10 '15 at 11:26
  • 1
    I'd like to improve this answer. If you can help with pointers on making it clearer it would be a great help. – Mihai Timar Jun 10 '15 at 11:26
  • I added your explanation to your answer. It makes a lot more sense now. – Jasper Jun 10 '15 at 12:42
  • Thank you! It does look nicer this way. – Mihai Timar Jun 11 '15 at 12:00
  • It looks great to just read the code. But doesn't solve problem! – Jayprakash Dubey Jun 19 '16 at 11:54
  • @JayprakashDubey it did work when I first answered. Where does it not work? Or does it do smth unwanted? Or does it not do smth you want? – Mihai Timar Jun 21 '16 at 09:03
  • I think this is a very clever solution. However, I noticed that it doesn't catch touches that occur inside a WKWebView. – Dorian Roy Sep 28 '17 at 13:22
3

Ultimately you need to define what you consider to be idle - is idle the result of the user not touching the screen or is it the state of the system if no computing resources are being used? It is possible, in many applications, for the user to be doing something even if not actively interacting with the device through the touch screen. While the user is probably familiar with the concept of the device going to sleep and the notice that it will happen via screen dimming, it is not necessarily the case that they'll expect something to happen if they are idle - you need to be careful about what you would do. But going back to the original statement - if you consider the 1st case to be your definition, there is no really easy way to do this. You'd need to receive each touch event, passing it along on the responder chain as needed while noting the time it was received. That will give you some basis for making the idle calculation. If you consider the second case to be your definition, you can play with an NSPostWhenIdle notification to try and perform your logic at that time.

wisequark
  • 3,288
  • 20
  • 12
  • 1
    Just to clarify, I am talking about interaction with the screen. I'll update the question to reflect that. – Mike McMaster Nov 07 '08 at 21:26
  • 1
    Then you can implement something where any time a touch happens you update a value which you check, or even set (and reset) an idle timer to fire, but you need to implement it yourself, because as wisequark said, what constitutes idle varies between different apps. – Louis Gerbarg Nov 07 '08 at 21:46
  • 1
    I am defining "idle" strictly as time since last touching the screen. I get that I'll need to implement it myself, I was just wondering what the "best" way would be to, say, intercept screen touches, or if someone knows of an alternative method to determine this. – Mike McMaster Nov 07 '08 at 21:55
1

Outside is 2021 and I would like share my approach to handle this without extending the UIApplication. I will not describe how to create a timer and reset it. But rather how to catch all events. So your AppDelegate starts with this:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

So all you need to do is to subclass UIWindow and override sendEvent, like below

import UIKit

class MyWindow: UIWindow {

    override func sendEvent(_ event: UIEvent){
        super.sendEvent(event)
        NSLog("Application received an event. Do whatever you want")
    }
}

And later create window with our class:

self.window = MyWindow(frame: UIScreen.main.bounds)
Dmitriy Mitiai
  • 1,112
  • 12
  • 15