IPhone: 检测自上次触屏以来用户的不活动/空闲时间

是否有人实现了这样一个特性: 如果用户在一段时间内没有触摸屏幕,你就会采取某种行动?我正在想最好的办法。

在 UIApplication 中有这样一个有些相关的方法:

[UIApplication sharedApplication].idleTimerDisabled;

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

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

然后,我可以设置一个计时器,定期检查这个值,当它超过阈值时采取一些行动。

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

75459 次浏览

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

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

在实现文件中,重写 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");

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

重点是:

Http://gist.github.com/365998

此外,UIApplication 子类问题的原因是,NIB 被设置为随后创建2个 UIApplication 对象,因为它包含应用程序和委托。但是 UIWindow 子类工作得很好。

实际上子类化的想法非常有效。只是不要让你的委托成为 UIApplication子类。创建从 UIApplication继承的另一个文件(例如 myApp)。在 IB 中,将 fileOwner对象的类设置为 myApp,在 myApp.m 中实现 sendEvent方法,如上所示。在 main. m do 中:

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

就是这样!

我有一个空闲计时器解决方案的变种,它不需要子类化 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

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

我只是遇到了这个问题,一个游戏是由动作控制,即屏幕锁定禁用,但应该在菜单模式下再次启用它。我没有使用计时器,而是将对 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在进入菜单或其他应该在空闲计时器激活情况下运行的程序时失效,enableIdleTimer会从您的应用委托的 applicationWillResignActive方法中调用,以确保所有的更改都被正确地重置为系统默认行为。
我写了一篇文章,并提供了单例类 IdleTimerManager 的代码。 iPhone 游戏中的空闲计时器处理

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

计时器是在 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];

对于迅捷 V3.1

不要忘记在应用委托 //@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)

有一种方法可以实现这个应用程序的宽度,而不需要单独的控制器做任何事情。只要添加一个手势识别器,不取消触摸。这样,所有的触摸都会被定时器跟踪,其他的触摸和手势不会受到任何影响,所以没有人会知道。

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 的方法,而不会阻碍其他人的功能。

外部是2021年,我想分享我的方法来处理这一点,而不扩展 UIApplication。我不会描述如何创建一个计时器和重置它。而是如何捕捉所有事件。所以你的应用委托是这样开始的:

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