【问题标题】:iOS perform action after period of inactivity (no user interaction)iOS 在一段时间不活动后执行操作(无用户交互)
【发布时间】:2011-12-26 11:27:47
【问题描述】:

如何向我的 iOS 应用添加基于用户交互(或缺少交互)的计时器?换句话说,如果 2 分钟内没有用户交互,我想让应用程序做一些事情,在这种情况下导航到初始视图控制器。如果在 1:55 有人触摸屏幕,计时器将重置。我认为这需要一个全局计时器,因此无论您在哪个视图上,缺乏交互都会启动计时器。虽然,我可以在每个视图上创建一个独特的计时器。有没有人有任何建议、链接或示例代码?

【问题讨论】:

标签: ios xcode timer


【解决方案1】:

Anne 提供的链接是一个很好的起点,但是,作为 n00b 的我,很难将其转化为我现有的项目。我发现了一个博客 [原始博客不再存在],它提供了更好的逐步说明,但它不是为 XCode 4.2 和使用情节提要编写的。以下是我如何让非活动计时器为我的应用程序工作的文章:

  1. 创建一个新文件 -> Objective-C 类 -> 输入一个名称(在我的例子中为 TIMERUIApplication)并将子类更改为 UIApplication。您可能必须在子类字段中手动键入。您现在应该拥有相应的 .h 和 .m 文件。

  2. 将.h文件改为如下:

    #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. 将.m文件改为如下:

    #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. 进入您的 Supporting Files 文件夹并将 main.m 更改为此(与以前版本的 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. 在您的 AppDelegate.m 文件中编写剩余的代码。我遗漏了与此过程无关的代码。 .h 文件无需更改。

    #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];
    }
    

注意:只要检测到触摸,计时器就会启动。这意味着如果用户触摸主屏幕(在我的情况下为“mainView”),即使没有离开该视图,相同的视图也会在分配的时间后自行推倒。对我的应用程序来说没什么大不了的,但对你的应用程序来说可能是。只有在识别到触摸后,计时器才会重置。如果您想在返回到您想要的页面后立即重置计时器,请将此代码包含在 ...pushViewController:controller animated:YES];

[(TIMERUIApplication *)[UIApplication sharedApplication] resetIdleTimer];

如果视图只是坐在那里没有交互,这将导致视图每 x 分钟推送一次。每次识别到触摸时,计时器仍会重置,因此仍然可以工作。

如果您提出改进建议,请发表评论,尤其是在“mainView”当前正在显示时禁用计时器。我似乎无法弄清楚我的 if 语句来让它注册当前视图。但我对自己所处的位置感到满意。下面是我对 if 语句的初步尝试,因此您可以看到我在哪里使用它。

-(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];
    }
}

我仍然是一个 n00b 并且可能没有以最好的方式完成所有事情。建议总是受欢迎的。

【讨论】:

  • 这是一个很好的演练,当然值得更多的支持。我也是一个 n00b,并且在我偶然发现这个之前,我一直在试图弄清楚如何实现这样的东西好几个星期。非常感谢。我要澄清的唯一一件事是设置您在 cmets 中提到的视图控制器的 ID,您在身份检查器中的 Identity 然后设置 Storyboard ID。但我真的只是在吹毛求疵。再次感谢。
  • 对于“n00b”(至少在那个时候),这是相当先进的东西。这很有帮助。谢谢! :)
  • 您好,感谢您提供以下方法,[(TIMERUIApplication *)[UIApplication sharedApplication] resetIdleTimer];
  • 您目前使用 AppDelegate 来收听您的通知,这使得很难确定“mainView”是否已被推送到 rootViewController。相反,将监听逻辑转移到您的 rootViewController。如果是 UINavigationController,请检查其 viewControllers 数组以查看是否存在“mainView”。如果是这样,什么也不做;否则将“mainView”实例推送到导航堆栈上。如果 rootViewController 只是一个 UIViewController 实例,你可以模态地呈现一个“mainView”实例。
  • 由于我只是在我继承的一个项目中逐字逐句地找到了这段代码,我认为值得指出的是关于时间单位的 cmets 是不正确的。头文件中的值代表分钟(不是秒),在 .m 文件中,我们将其转换为秒(不是分钟)以用作 NSTimeInterval。
【解决方案2】:

我已经实现了 Bobby 的建议,但在 Swift 中。代码概述如下。

  1. 创建一个新文件 -> Swift File -> 输入一个名称(在我的例子中 TimerUIApplication) 并将子类更改为 UIApplication。改变 TimerUIApplication.swift 文件读取如下:

    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. 创建一个新文件 -> Swift 文件 -> main.swift (名字是 重要)。

    import UIKit
    
    UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(TimerUIApplication), NSStringFromClass(AppDelegate))
    
  3. 在您的 AppDelegate 中:删除上面的 @UIApplicationMain 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")
                }
            }
        }
    }
    

请记住,您可能需要在 applicationDidTimout 中执行不同的操作,具体取决于您的根视图控制器。请参阅this post 以获取有关如何投射视图控制器的更多详细信息。如果您在导航控制器上有模态视图,您可能需要使用visibleViewController 而不是topViewController

【讨论】:

  • 嘿,@Vanessa Forney ......我如何覆盖以下变量............ TimerUIApplication 类的 timeoutInSeconds 来自我的视图控制器
  • @Vanessa Forney : Process.argc 来自哪里?
  • @EKChhuon 我认为它已重命名为 CommandLine.argc 所以也许试一试,但我不确定这个答案的有效性了。
  • 您的解决方案有效。只需要在 main.swift 文件中做一些更改。 UIApplicationMain( CommandLine.argc, UnsafeMutableRawPointer(CommandLine.unsafeArgv) .bindMemory( to: UnsafeMutablePointer&lt;Int8&gt;.self, capacity: Int(CommandLine.argc)), NSStringFromClass(TimerUIApplication.self), NSStringFromClass(AppDelegate.self) )
【解决方案3】:

背景 [Swift 解决方案]

有人要求用 Swift 更新这个答案,所以我在下面添加了一个 sn-p。

请注意,我已经为自己的用途修改了一些规范:如果 5 秒内没有UIEvents,我基本上想做工作。任何传入的触摸UIEvent 都会取消之前的计时器并使用新的计时器重新开始。

与上述答案的区别

  • 与上面accepted answer 的一些变化:我没有在第一个事件时设置第一个计时器,而是立即在init() 中设置了我的计时器。此外,我的reset_idle_timer() 将取消前一个计时器,因此任何时候都只会运行一个计时器。

重要提示:构建前的 2 个步骤

感谢一些关于 SO 的精彩回答,我能够将上面的代码改编为 Swift 代码。

  • 关注 this answer,了解如何在 Swift 中继承 UIApplication。确保您遵循 Swift 的这些步骤,否则下面的 sn-p 将无法编译。由于链接的答案很好地描述了这些步骤,我不会在这里重复。阅读并正确设置它应该花费您不到一分钟的时间。

  • 我无法让NSTimercancelPreviousPerformRequestsWithTarget: 工作,所以我发现这个updated GCD solution 效果很好。只需将该代码放到一个单独的 .swift 文件中,您就是 gtg(因此您可以调用 delay()cancel_delay(),并使用 dispatch_cancelable_closure)。

恕我直言,下面的代码很简单,任何人都可以理解。对于没有回答有关此答案的任何问题,我提前道歉(工作 atm 有点泛滥)。

我刚刚发布了这个答案,以回馈我所获得的重要信息。

片段

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()
    }
}

【讨论】:

  • super.sendEvent( event ) - 因此当我用力按下 (3d touch) 时它会崩溃...有什么办法可以解决这个问题吗?
【解决方案4】:

注意:只要检测到触摸,计时器就会启动。这意味着 如果用户触摸主屏幕(在我的情况下为“mainView”),甚至 不离开该视图,相同的视图将推过去 在规定的时间后自己。对我的应用来说没什么大不了的,但对于 可能是你的。定时器只会在触摸后重置 认可。如果您想在返回后立即重置计时器 你想进入的页面,在后面加上这个代码 ...pushViewController:控制器动画:YES];

解决再次开始显示同一视图的问题的一种方法是在 appdelegate 中设置一个 BOOL,并在您要检查用户是否空闲时将其设置为 true,并在您移动到空闲时将其设置为 false看法。然后在 TIMERUIApplication 中的 idleTimerExceeded 方法中有一个 if 语句,如下所示。在要检查用户开始空闲的所有视图的 viewDidload 视图中,将 appdelegate.idle 设置为 true,如果有其他视图不需要检查用户是否处于空闲状态,则可以将其设置为 false .

-(void)idleTimerExceeded{
          AppDelegate *appdelegate = [[UIApplication sharedApplication] delegate];

          if(appdelegate.idle){
            [[NSNotificationCenter defaultCenter] postNotificationName: kApplicationDidTimeOutNotification object:nil]; 
          }
}

【讨论】:

  • 好电话,大卫。感谢您跟进这个想法!我无法在最初使用上述代码设计的应用程序中实现此修复,但我肯定会在以后使用计时器技术的任何项目中引用它。
  • 没问题 BobbyScon。我目前正在开发一个也需要此功能的应用程序,即空闲活动计时器,我发现您实现空闲计时器的方式比我最初设置自己的方式效果要好得多。很高兴能够以一些小的方式做出贡献。
【解决方案5】:

此处为 Swift 3 示例

  1. 创建一个类似的类。

     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()
    
    
       }
    }
    
  2. 创建另一个名为 ma​​in.swift 的类,粘贴下面的代码

    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))
            }
    
  3. 别忘了从 AppDelegate 中移除 @UIApplicationMain

  4. Swift 3 完整源代码已提供给 GitHub。 GitHub链接:https://github.com/enamul95/UserInactivity

【讨论】:

  • 当我卸载应用程序时它会崩溃......所以没有调用applicationDidFinishLaunching......有没有办法解决这个问题?
【解决方案6】:

Vanessa 的答案中子类UIApplication 的 Swift 3.0 转换

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()
                }
            }
        }

    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-01-27
    • 2018-12-19
    • 1970-01-01
    • 2011-10-01
    • 1970-01-01
    • 2020-03-16
    • 2022-08-16
    相关资源
    最近更新 更多