Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS perform action after period of inactivity (no user interaction)

Tags:

xcode

ios

timer

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 app do something, in this case navigate to the initial view controller. If at 1:55 someone touches the screen, the timer resets. I would think this would need to be a global timer so no matter which view you are on, the lack of interaction starts the timer. Although, I could create a unique timer on each view. Does anyone have any suggestions, links or sample code where this has been done before?

like image 824
BobbyScon Avatar asked Nov 10 '11 19:11

BobbyScon


Video Answer


2 Answers

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:

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

  2. 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 
  3. 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 
  4. 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]));     } } 
  5. 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.

like image 60
BobbyScon Avatar answered Oct 20 '22 01:10

BobbyScon


I have implemented what Bobby has suggested, but in Swift. The code is outlined below.

  1. 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)     } } 
  2. Create a new file -> Swift File -> main.swift (the name is important).

    import UIKit  UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(TimerUIApplication), NSStringFromClass(AppDelegate)) 
  3. 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.

like image 37
Vanessa Forney Avatar answered Oct 20 '22 01:10

Vanessa Forney