iOS Perform Action After Period of Inactivity (No User Interaction)

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

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 

    //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 

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

Detecting inactivity (no user interaction) in iOS for showing separate Screen like an Overlay

From @Prabhat Kesara's comment and @Vanessa Forney answer in the helper link. I come with the below solution and it is working fine. If someone comes with the requirement like this they can use this

import UIKit
import Foundation

extension NSNotification.Name {
public static let TimeOutUserInteraction: NSNotification.Name = NSNotification.Name(rawValue: "TimeOutUserInteraction")
}

class UserInterractionSetup: UIApplication {

static let ApplicationDidTimoutNotification = "AppTimout"

// The timeout in seconds for when to fire the idle timer.

let timeoutInSeconds: TimeInterval = 1 * 60 //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 idleTimer != nil {

self.resetIdleTimer()

}

if let touches = event.allTouches {

for touch in touches {

if touch.phase == UITouch.Phase.began {

self.resetIdleTimer()

}

}

}

}


// Resent the timer because there was user interaction.

func resetIdleTimer() {

if let idleTimer = idleTimer {

// print("1")

let root = UIApplication.shared.keyWindow?.rootViewController

root?.dismiss(animated: true, completion: nil)

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.

@objc func idleTimerExceeded() {

print("Time Out")
NotificationCenter.default.post(name:Notification.Name.TimeOutUserInteraction, object: nil)

let screenSaverVC = UIStoryboard(name:"ScreenSaver", bundle:nil).instantiateViewController(withIdentifier:"LandingPageViewController") as! LandingPageViewController

if let presentVc = TopMostViewController.sharedInstance.topMostViewController(controller: screenSaverVC){


let root: UINavigationController = UIApplication.shared.keyWindow!.rootViewController as! UINavigationController

if let topView = root.viewControllers.last?.presentedViewController {

topView.dismiss(animated: false) {

root.viewControllers.last?.navigationController?.present(presentVc, animated: true, completion: nil)

}

}else if let topViewNavigation = root.viewControllers.last {

topViewNavigation.navigationController?.present(presentVc, animated: true, completion: nil)



}

}

}

}

the difficulty I was faced was presenting overlay or screensaver above an already presented a view. That case, also we are handling in the above solution.
Happy coding :)

iPhone: Detecting user inactivity/idle time since last screen touch

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");


Related Topics



Leave a reply



Submit