【问题标题】:iPhone: Detecting user inactivity/idle time since last screen touchiPhone:检测自上次屏幕触摸以来的用户不活动/空闲时间
【发布时间】:2010-09-21 08:50:03
【问题描述】:

有没有人实现了一个功能,如果用户在一段时间内没有触摸屏幕,你就会采取某种行动?我正在尝试找出最好的方法。

在 UIApplication 中有这个有点相关的方法:

[UIApplication sharedApplication].idleTimerDisabled;

如果你有这样的东西就好了:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

然后我可以设置一个计时器并定期检查这个值,并在它超过阈值时采取一些措施。

希望这能解释我在寻找什么。有没有人已经解决了这个问题,或者对你将如何做有任何想法?谢谢。

【问题讨论】:

  • 这是一个很好的问题。 Windows 有 OnIdle 事件的概念,但我认为这更多是关于应用程序当前未处理其消息泵中的任何内容,而 iOS idleTimerDisabled 属性似乎只与锁定设备有关。任何人都知道在 iOS/MacOSX 中是否有任何与 Windows 概念相近的东西?

标签: ios objective-c iphone idle-timer


【解决方案1】:

这是我一直在寻找的答案:

让您的应用程序委托子类 UIApplication。在实现文件中,重写 sendEvent: 方法,如下所示:

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

其中 maxIdleTime 和 idleTimer 是实例变量。

为了使其工作,您还需要修改 main.m 以告诉 UIApplicationMain 使用您的委托类(在本例中为 AppDelegate)作为主体类:

int retVal = UIApplicationMain(argc, argv, @"AppDelegate", @"AppDelegate");

【讨论】:

  • 嗨,迈克,我的 AppDelegate 是从 NSObject 继承的,所以更改了 UIApplication 并实施上述方法来检测用户变得空闲,但我收到错误“由于未捕获的异常 'NSInternalInconsistencyException' 而终止应用程序,原因:'那里只能是一个 UIApplication 实例。'".. 我还需要做什么...?
  • 我要补充一点,UIApplication 子类应该与 UIApplicationDelegate 子类分开
  • 我不确定当计时器停止触发时设备进入非活动状态时这将如何工作?
  • 如果我为超时事件指定使用 popToRootViewController 函数,则无法正常工作。它发生在我显示 UIAlertView,然后是 popToRootViewController,然后我按下 UIAlertView 上的任何按钮,其中的选择器来自已弹出的 uiviewController
  • 非常好!但是,如果有很多触摸,这种方法会创建很多 NSTimer 实例。
【解决方案2】:

我有一个空闲计时器解决方案的变体,它不需要子类化 UIApplication。它适用于特定的 UIViewController 子类,因此如果您只有一个视图控制器(如交互式应用程序或游戏可能有)或只想处理特定视图控制器中的空闲超时,则很有用。

它也不会在每次重置空闲计时器时重新创建 NSTimer 对象。如果计时器触发,它只会创建一个新的。

您的代码可以调用resetIdleTimer 处理可能需要使空闲计时器无效的任何其他事件(例如重要的加速度计输入)。

@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

(为简洁起见,排除了内存清理代码。)

【讨论】:

  • 非常好。这个答案很震撼!击败标记为正确的答案,虽然我知道这很早,但现在这是一个更好的解决方案。
  • 这很好,但我发现了一个问题:在 UITableViews 中滚动不会导致调用 nextResponder。我还尝试通过 touchesBegan: 和 touchesMoved: 进行跟踪,但没有任何改进。有什么想法吗?
  • @GregMaletic:我遇到了同样的问题,但最后我添加了 - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { NSLog(@"Will begin dragging"); } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { NSLog(@"Did Scroll"); [自我重置空闲计时器];你试过这个吗?
  • 谢谢。这仍然很有帮助。我将它移植到 Swift 上,效果很好。
  • 你是个摇滚明星。荣誉
【解决方案3】:

对于 swift v 3.1

别忘了在 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)
   }
} 

创建 main.swif 文件并添加它(名称很重要)

CommandLine.unsafeArgv.withMemoryRebound(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)) {argv in
_ = UIApplicationMain(CommandLine.argc, argv, NSStringFromClass(InterractionUIApplication.self), NSStringFromClass(AppDelegate.self))
}

在任何其他类中观察通知

NotificationCenter.default.addObserver(self, selector: #selector(someFuncitonName), name: Notification.Name.TimeOutUserInteraction, object: nil)

【讨论】:

  • 我不明白为什么我们需要在sendEvent() 方法中检查if idleTimer != nil
  • 我们如何从 Web 服务响应中为 timeoutInSeconds 设置值?
【解决方案4】:

这个线程很有帮助,我将它封装到一个发送通知的 UIWindow 子类中。我选择通知是为了让它成为真正的松耦合,但你可以很容易地添加一个委托。

这里是要点:

http://gist.github.com/365998

另外,UIApplication 子类问题的原因是,NIB 设置为创建 2 个 UIApplication 对象,因为它包含应用程序和委托。 UIWindow 子类虽然效果很好。

【讨论】:

  • 你能告诉我如何使用你的代码吗?我不明白怎么称呼它
  • 它非常适合触摸,但似乎无法处理来自键盘的输入。这意味着如果用户在 gui 键盘上输入内容,它将超时。
  • 我也无法理解如何使用它......我在我的视图控制器中添加了观察者,并期望在应用程序未触及/空闲时触发通知......但什么也没发生......加上来自我们在哪里可以控制空闲时间?就像我想要 120 秒的空闲时间,以便在 120 秒后 IdleNotification 应该触发,而不是在那之前。
【解决方案5】:

实际上,子类化的想法很有效。只是不要让您的代表成为UIApplication 子类。创建另一个继承自 UIApplication 的文件(例如 myApp)。在 IB 中,将 fileOwner 对象的类设置为 myApp,并在 myApp.m 中实现上述 sendEvent 方法。在 main.m 中:

int retVal = UIApplicationMain(argc,argv,@"myApp.m",@"myApp.m")

瞧!

【讨论】:

【解决方案6】:

我刚刚在一个由动作控制的游戏中遇到了这个问题,即禁用了屏幕锁定,但在菜单模式下应该再次启用它。我没有使用计时器,而是将所有对 setIdleTimerDisabled 的调用封装在一个提供以下方法的小类中:

- (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 停用空闲计时器,enableIdleTimerDelayed 进入菜单时或任何应在空闲计时器处于活动状态的情况下运行,并且从 AppDelegate 的 applicationWillResignActive 方法调用 enableIdleTimer 以确保您的所有更改都正确重置为系统默认值行为。
我写了一篇文章,提供了单例类IdleTimerManager的代码Idle Timer Handling in iPhone Games

【讨论】:

    【解决方案7】:

    这是检测活动的另一种方法:

    定时器是在UITrackingRunLoopMode 中添加的,所以只有在有UITracking 活动时才会触发。它还有一个很好的优势,即不会向您发送所有触摸事件的垃圾邮件,从而通知您在最后ACTIVITY_DETECT_TIMER_RESOLUTION 秒内是否有活动。我将选择器命名为keepAlive,因为这似乎是一个合适的用例。您当然可以根据最近有活动的信息做任何您想做的事情。

    _touchesTimer = [NSTimer timerWithTimeInterval:ACTIVITY_DETECT_TIMER_RESOLUTION
                                            target:self
                                          selector:@selector(keepAlive)
                                          userInfo:nil
                                           repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:_touchesTimer forMode:UITrackingRunLoopMode];
    

    【讨论】:

    • 怎么回事?我相信很明显你应该让你自己“keepAlive”选择器来满足你的任何需求。也许我错过了你的观点?
    • 你说这是另一种检测活动的方法,但是,这只会实例化一个 iVar,它是一个 NSTimer。我不明白这如何回答 OP 的问题。
    • UITrackingRunLoopMode 中添加了计时器,因此只有在有 UITracking 活动时才会触发。它还有一个很好的优势,即不会向您发送所有触摸事件的垃圾邮件,从而通知您在最后的 ACTIVITY_DETECT_TIMER_RESOLUTION 秒内是否有活动。我将选择器命名为 keepAlive,因为它似乎是一个合适的用例。您当然可以根据最近有活动的信息做任何您想做的事情。
    • 我想改进这个答案。如果您能提供有关使其更清晰的指示,那将是一个很大的帮助。
    • 我在您的回答中添加了您的解释。现在更有意义了。
    【解决方案8】:

    有一种方法可以在整个应用范围内执行此应用,而无需单个控制器执行任何操作。只需添加一个不会取消触摸的手势识别器。这样一来,所有的触摸都会被计时器跟踪,而其他触摸和手势完全不受影响,因此其他人不必知道。

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

    在您的应用委托的完成启动方法中,只需调用 addGesture 即可。所有触摸都将通过 CatchAllGesture 的方法,而不会妨碍其他人的功能。

    【讨论】:

    • 我喜欢这种方法,将它用于 Xamarin 的类似问题:stackoverflow.com/a/51727021/250164
    • 效果很好,似乎这种技术也用于控制 AVPlayerViewController (private API ref) 中 UI 控件的可见性。覆盖应用程序-sendEvent: 是矫枉过正,UITrackingRunLoopMode 不能处理很多情况。
    • @RomanB。是的。当您使用 iOS 的时间足够长时,您知道始终使用“正确的方式”,这是按预期实现自定义手势的直接方式 developer.apple.com/documentation/uikit/uigesturerecognizer/…
    • 在 touchesEnded 中将状态设置为 .failed 有什么作用?
    • 我喜欢这种方法,但在尝试时,它似乎只能捕捉到点击,而不是任何其他手势,如平移、滑动等 touchesEnded 重置逻辑所在的位置。这是故意的吗?
    【解决方案9】:

    最终,您需要定义您认为空闲的内容 - 空闲是用户未触摸屏幕的结果,还是未使用计算资源时系统的状态?在许多应用程序中,即使没有通过触摸屏主动与设备交互,用户也有可能正在做某事。虽然用户可能熟悉设备进入睡眠状态的概念并注意到它将通过屏幕变暗发生,但他们不一定会期望在空闲时发生某些事情 - 你需要小心关于你会做什么。但回到最初的陈述——如果你认为第一种情况是你的定义,那么没有真正简单的方法可以做到这一点。您需要接收每个触摸事件,并根据需要将其传递到响应者链上,同时注意接收时间。这将为您进行空闲计算提供一些基础。如果您认为第二种情况是您的定义,您可以使用 NSPostWhenIdle 通知来尝试在那时执行您的逻辑。

    【讨论】:

    • 澄清一下,我说的是与屏幕的交互。我会更新问题以反映这一点。
    • 然后你可以实现一些东西,每当触摸发生时你更新你检查的值,甚至设置(和重置)一个空闲计时器来触发,但你需要自己实现它,因为wisequark 说,不同的应用程序之间的空闲是什么。
    • 我将“空闲”严格定义为自上次触摸屏幕以来的时间。我知道我需要自己实现它,我只是想知道“最好”的方法是什么,比如拦截屏幕触摸,或者是否有人知道另一种方法来确定这一点。
    【解决方案10】:

    外面是 2021 年,我想分享我在不扩展 UIApplication 的情况下处理此问题的方法。我不会描述如何创建计时器并重置它。而是如何捕捉所有事件。所以你的 AppDelegate 以这个开头:

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

    所以你需要做的就是继承 UIWindow 并覆盖sendEvent,如下所示

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

    然后用我们的类创建窗口:

    self.window = MyWindow(frame: UIScreen.main.bounds)
    

    【讨论】:

    • 非常感谢!效果很好。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-08-08
    • 1970-01-01
    • 2012-03-04
    相关资源
    最近更新 更多