English 中文(简体)
iPhone:检测用户不活跃/闲置时间,自上次屏幕触摸以来。
原标题:
  • 时间:2008-11-07 20:02:59
  •  标签:

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

UIApplication 中有一个有关联的方法:

[UIApplication sharedApplication].idleTimerDisabled;

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

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

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

希望这可以解释我正在寻找的东西。是否已经有人解决了这个问题,或者你们有什么想法如何做到这一点?谢谢。

最佳回答

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

请将您的应用程序委托子类化为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");
问题回答

我有一种不需要子类化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

(为简洁起见,内存清理代码已省略。)

对于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)

这个帖子非常有帮助,我把它封装成了一个发送通知的UIWindow子类。我选择通知使它成为真正的松耦合,但您也可以很容易地添加代理。

这是要点:

将此翻译为中文:http://gist.github.com/365998 http://gist.github.com/365998

同样,UIApplication子类问题的原因是NIB已设置为创建两个UIApplication对象,因为它包含应用程序和委托。UIWindow子类运作良好。

有一种方法可以在整个应用程序中执行此操作,而无需各个控制器进行任何操作。只需添加一个不取消触摸的手势识别器。这样,所有触摸都将被跟踪计时器,其他触摸和手势则不会受到影响,因此其他人不必知道它。

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

在您的应用程序委托的didFinishLaunch方法中,只需调用addGesture即可。所有触摸都将通过CatchAllGesture的方法,而不会阻止其他功能。

实际上,子类化的想法非常好。只是不要将您的委托设置为UIApplication子类。创建另一个文件,该文件从UIApplication继承(例如,myApp)。在IB中将fileOwner对象的类设置为myApp,并在myApp.m中实现sendEvent方法。在main.m中进行:

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

这就是它!

我刚刚遇到一个问题,有一个通过动作控制的游戏,即屏幕锁定已禁用,但在菜单模式下应重新启用它。我将所有对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 deactivates idle timer, enableIdleTimerDelayed when entering the menu or whatever should run with idle timer active and enableIdleTimer is called from your AppDelegate s applicationWillResignActive method to ensure all your changes are reset properly to the system default behaviour.
I wrote an article and provided the code for the singleton class IdleTimerManager Idle Timer Handling in iPhone Games

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

定时器被添加在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];

最终,您需要定义您认为什么是空闲的——是用户没有触摸屏幕的结果,还是在没有使用计算资源的情况下系统的状态?在许多应用程序中,即使用户没有通过触摸屏幕进行积极交互,他们仍可能在做某些事情。虽然用户可能熟悉设备进入睡眠状态和通过屏幕变暗发出的通知,但并不一定意味着他们期望在空闲时会发生什么——您需要小心考虑您会做什么。但是回到原始声明——如果您认为第一种情况是您的定义,那么没有真正简单的方法来做到这一点。您需要接收每个触摸事件,根据需要将其传递到响应者链,并注意接收时间。这将为您提供制定空闲计算的基础。如果您认为第二种情况是您的定义,则可以使用NSPostWhenIdle通知尝试在那个时候执行您的逻辑。

外面是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)




相关问题
热门标签