How can I add a timer to my iOS app that is based on user interaction (or lack thereof)? In other words, if there is no user interaction for 2 minutes, I want to have the ap
Swift 3.0 Conversion of the subclassed UIApplication
in Vanessa's Answer
class TimerUIApplication: UIApplication {
static let ApplicationDidTimoutNotification = "AppTimout"
// The timeout in seconds for when to fire the idle timer.
let timeoutInSeconds: TimeInterval = 5 * 60
var idleTimer: Timer?
// 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(TimerUIApplication.idleTimerExceeded), userInfo: nil, repeats: false)
}
// If the timer reaches the limit as defined in timeoutInSeconds, post this notification.
func idleTimerExceeded() {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: TimerUIApplication.ApplicationDidTimoutNotification), object: nil)
}
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()
}
}
}
}
}
The link that Anne provided was a great starting point, but, being the n00b that I am, it was difficult to translate into my existing project. I found a blog [original blog no longer exists] that gave a better step-by-step, but it wasn't written for XCode 4.2 and using storyboards. Here is a write up of how I got the inactivity timer to work for my app:
Create a new file -> Objective-C class -> type in a name (in my case TIMERUIApplication) and change the subclass to UIApplication. You may have to manually type this in the subclass field. You should now have the appropriate .h and .m files.
Change the .h file to read as follows:
#import <Foundation/Foundation.h>
//the length of time before your application "times out". This number actually represents seconds, so we'll have to multiple it by 60 in the .m file
#define kApplicationTimeoutInMinutes 5
//the notification your AppDelegate needs to watch for in order to know that it has indeed "timed out"
#define kApplicationDidTimeoutNotification @"AppTimeOut"
@interface TIMERUIApplication : UIApplication
{
NSTimer *myidleTimer;
}
-(void)resetIdleTimer;
@end
Change the .m file to read as follows:
#import "TIMERUIApplication.h"
@implementation TIMERUIApplication
//here we are listening for any touch. If the screen receives touch, the timer is reset
-(void)sendEvent:(UIEvent *)event
{
[super sendEvent:event];
if (!myidleTimer)
{
[self resetIdleTimer];
}
NSSet *allTouches = [event allTouches];
if ([allTouches count] > 0)
{
UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;
if (phase == UITouchPhaseBegan || phase == UITouchPhaseMoved)
{
[self resetIdleTimer];
}
}
}
//as labeled...reset the timer
-(void)resetIdleTimer
{
if (myidleTimer)
{
[myidleTimer invalidate];
}
//convert the wait period into minutes rather than seconds
int timeout = kApplicationTimeoutInMinutes * 60;
myidleTimer = [NSTimer scheduledTimerWithTimeInterval:timeout target:self selector:@selector(idleTimerExceeded) userInfo:nil repeats:NO];
}
//if the timer reaches the limit as defined in kApplicationTimeoutInMinutes, post this notification
-(void)idleTimerExceeded
{
[[NSNotificationCenter defaultCenter] postNotificationName:kApplicationDidTimeoutNotification object:nil];
}
@end
Go into your Supporting Files folder and alter main.m to this (different from prior versions of XCode):
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "TIMERUIApplication.h"
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, NSStringFromClass([TIMERUIApplication class]), NSStringFromClass([AppDelegate class]));
}
}
Write the remaining code in your AppDelegate.m file. I've left out code not pertaining to this process. There is no change to make in the .h file.
#import "AppDelegate.h"
#import "TIMERUIApplication.h"
@implementation AppDelegate
@synthesize window = _window;
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidTimeout:) name:kApplicationDidTimeoutNotification object:nil];
return YES;
}
-(void)applicationDidTimeout:(NSNotification *) notif
{
NSLog (@"time exceeded!!");
//This is where storyboarding vs xib files comes in. Whichever view controller you want to revert back to, on your storyboard, make sure it is given the identifier that matches the following code. In my case, "mainView". My storyboard file is called MainStoryboard.storyboard, so make sure your file name matches the storyboardWithName property.
UIViewController *controller = [[UIStoryboard storyboardWithName:@"MainStoryboard" bundle:NULL] instantiateViewControllerWithIdentifier:@"mainView"];
[(UINavigationController *)self.window.rootViewController pushViewController:controller animated:YES];
}
Notes: The timer will start anytime a touch is detected. This means that if the user touches the main screen (in my case "mainView") even without navigating away from that view, the same view will push over itself after the allotted time. Not a big deal for my app, but for yours it might be. The timer will only reset once a touch is recognized. If you want to reset the timer as soon as you get back to the page you want to be at, include this code after the ...pushViewController:controller animated:YES];
[(TIMERUIApplication *)[UIApplication sharedApplication] resetIdleTimer];
This will cause the view to push every x minutes if it's just sitting there with no interaction. The timer will still reset every time it recognizes a touch, so that will still work.
Please comment if you have suggested improvements, especially someway to disable the timer if the "mainView" is currently being displayed. I can't seem to figure out my if statement to get it to register the current view. But I'm happy with where I'm at. Below is my initial attempt at the if statement so you can see where I was going with it.
-(void)applicationDidTimeout:(NSNotification *) notif
{
NSLog (@"time exceeded!!");
UIViewController *controller = [[UIStoryboard storyboardWithName:@"MainStoryboard" bundle:NULL] instantiateViewControllerWithIdentifier:@"mainView"];
//I've tried a few varieties of the if statement to no avail. Always goes to else.
if ([controller isViewLoaded]) {
NSLog(@"Already there!");
}
else {
NSLog(@"go home");
[(UINavigationController *)self.window.rootViewController pushViewController:controller animated:YES];
//[(TIMERUIApplication *)[UIApplication sharedApplication] resetIdleTimer];
}
}
I am still a n00b and may have not done everything the best way. Suggestions are always welcome.
Notes: The timer will start anytime a touch is detected. This means that if the user touches the main screen (in my case "mainView") even without navigating away from that view, the same view will push over itself after the allotted time. Not a big deal for my app, but for yours it might be. The timer will only reset once a touch is recognized. If you want to reset the timer as soon as you get back to the page you want to be at, include this code after the ...pushViewController:controller animated:YES];
One solution to this problem of the same view beginning displayed again is to have a BOOL in the appdelegate and set this to true when you want to check for the user being idle and setting this to false when you have moved to the idle view. Then in the TIMERUIApplication in the idleTimerExceeded method have an if statement as below. In the viewDidload view of all the views where you want to check for the user beginning idle you set the appdelegate.idle to true, if there are other views where you do not need to check for the user being idle you can set this to false.
-(void)idleTimerExceeded{
AppDelegate *appdelegate = [[UIApplication sharedApplication] delegate];
if(appdelegate.idle){
[[NSNotificationCenter defaultCenter] postNotificationName: kApplicationDidTimeOutNotification object:nil];
}
}
I have implemented what Bobby has suggested, but in Swift. The code is outlined below.
Create a new file -> Swift File -> type in a name (in my case TimerUIApplication) and change the subclass to UIApplication. Change the TimerUIApplication.swift file to read as follows:
class TimerUIApplication: UIApplication {
static let ApplicationDidTimoutNotification = "AppTimout"
// The timeout in seconds for when to fire the idle timer.
let timeoutInSeconds: TimeInterval = 5 * 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 event.allTouches?.contains(where: { $0.phase == .began || $0.phase == .moved }) == true {
resetIdleTimer()
}
}
// Resent the timer because there was user interaction.
func resetIdleTimer() {
idleTimer?.invalidate()
idleTimer = Timer.scheduledTimer(timeInterval: timeoutInSeconds, target: self, selector: #selector(AppDelegate.idleTimerExceeded), userInfo: nil, repeats: false)
}
// If the timer reaches the limit as defined in timeoutInSeconds, post this notification.
func idleTimerExceeded() {
Foundation.NotificationCenter.default.post(name: NSNotification.Name(rawValue: TimerUIApplication.ApplicationDidTimoutNotification), object: nil)
}
}
Create a new file -> Swift File -> main.swift (the name is important).
import UIKit
UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(TimerUIApplication), NSStringFromClass(AppDelegate))
In your AppDelegate: Remove @UIApplicationMain
above the
AppDelegate.
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(AppDelegate.applicationDidTimout(_:)), name: TimerUIApplication.ApplicationDidTimoutNotification, object: nil)
return true
}
...
// The callback for when the timeout was fired.
func applicationDidTimout(notification: NSNotification) {
if let vc = self.window?.rootViewController as? UINavigationController {
if let myTableViewController = vc.visibleViewController as? MyMainViewController {
// Call a function defined in your view controller.
myMainViewController.userIdle()
} else {
// We are not on the main view controller. Here, you could segue to the desired class.
let storyboard = UIStoryboard(name: "MyStoryboard", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("myStoryboardIdentifier")
}
}
}
}
Keep in mind you may have to do different things in applicationDidTimout depending on your root view controller. See this post for more details on how you should cast your view controller. If you have modal views over the navigation controller, you may want to use visibleViewController instead of topViewController.
Background [Swift Solution]
There was a request to update this answer with Swift so I've added a snippet below.
Do note that I have modified the specs somewhat for my own uses: I essentially want to do work if there are no UIEvents
for 5 seconds. Any incoming touch UIEvent
will cancel previous timers and restart with a new timer.
Differences from Answer Above
init()
immediately. Also my reset_idle_timer()
will cancel the previous timer so only one timer will be running at any time.IMPORTANT: 2 Steps Before Building
Thanks to a couple great answers on SO, I was able to adapt the code above as Swift code.
Follow this answer for a rundown on how to subclass UIApplication
in Swift. Make sure you follow those steps for Swift or the snippet below won't compile. Since the linked answer described the steps so well, I will not repeat here. It should take you less than a minute to read and set it up properly.
I could not get NSTimer
's cancelPreviousPerformRequestsWithTarget:
to work, so I found this updated GCD solution which works great. Just drop that code into a separate .swift file and you are gtg (so you can call delay()
and cancel_delay()
, and use dispatch_cancelable_closure
).
IMHO, the code below is simple enough for anyone to understand. I apologise in advance for not answering any questions on this answer (a bit flooded with work atm).
I just posted this answer to contribute back to SO what great information I've gotten out.
Snippet
import UIKit
import Foundation
private let g_secs = 5.0
class MYApplication: UIApplication
{
var idle_timer : dispatch_cancelable_closure?
override init()
{
super.init()
reset_idle_timer()
}
override func sendEvent( event: UIEvent )
{
super.sendEvent( event )
if let all_touches = event.allTouches() {
if ( all_touches.count > 0 ) {
let phase = (all_touches.anyObject() as UITouch).phase
if phase == UITouchPhase.Began {
reset_idle_timer()
}
}
}
}
private func reset_idle_timer()
{
cancel_delay( idle_timer )
idle_timer = delay( g_secs ) { self.idle_timer_exceeded() }
}
func idle_timer_exceeded()
{
println( "Ring ----------------------- Do some Idle Work!" )
reset_idle_timer()
}
}
Swift 3 example here
create a class like .
import Foundation
import UIKit
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//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)
// print("3")
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 {
// print("1")
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() {
print("Time Out")
NotificationCenter.default.post(name:Notification.Name.TimeOutUserInteraction, object: nil)
//Go Main page after 15 second
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window = UIWindow(frame: UIScreen.main.bounds)
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let yourVC = mainStoryboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
appDelegate.window?.rootViewController = yourVC
appDelegate.window?.makeKeyAndVisible()
}
}
create another class named main.swift paste bellow code
import Foundation
import UIKit
CommandLine.unsafeArgv.withMemoryRebound(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc))
{ argv in
_ = UIApplicationMain(CommandLine.argc, argv, NSStringFromClass(InterractionUIApplication.self), NSStringFromClass(AppDelegate.self))
}
don't forget to Remove @UIApplicationMain from AppDelegate
Swift 3 complete source code is given to GitHub. GitHub link:https://github.com/enamul95/UserInactivity