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

发布于 2024-07-09 05:52:07 字数 422 浏览 15 评论 0原文

有没有人实现了一项功能,如果用户在一段时间内没有触摸屏幕,您会采取特定的操作? 我正在努力找出最好的方法来做到这一点。

UIApplication 中有一个与此有些相关的方法:

[UIApplication sharedApplication].idleTimerDisabled;

如果您有这样的方法那就太好了:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

然后我可以设置一个计时器并定期检查该值,并在超过阈值时采取一些操作。

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

Has anybody implemented a feature where if the user has not touched the screen for a certain time period, you take a certain action? I'm trying to figure out the best way to do that.

There's this somewhat-related method in UIApplication:

[UIApplication sharedApplication].idleTimerDisabled;

It'd be nice if you instead had something like this:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Then I could set up a timer and periodically check this value, and take some action when it exceeds a threshold.

Hopefully that explains what I'm looking for. Has anyone tackled this issue already, or have any thoughts on how you would do it? Thanks.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

因为看清所以看轻 2024-07-16 05:52:07

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

让您的应用程序委托子类 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");

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");
静谧 2024-07-16 05:52:07

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

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

I have a variation of the idle timer solution which doesn't require subclassing UIApplication. It works on a specific UIViewController subclass, so is useful if you only have one view controller (like an interactive app or game may have) or only want to handle idle timeout in a specific view controller.

It also does not re-create the NSTimer object every time the idle timer is reset. It only creates a new one if the timer fires.

Your code can call resetIdleTimer for any other events that may need to invalidate the idle timer (such as significant accelerometer input).

@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

(memory cleanup code excluded for brevity.)

难以启齿的温柔 2024-07-16 05:52:07

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

For swift v 3.1

dont't forget comment this line in 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)
   }
} 

create main.swif file and add this (name is important)

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

Observing notification in an any other class

NotificationCenter.default.addObserver(self, selector: #selector(someFuncitonName), name: Notification.Name.TimeOutUserInteraction, object: nil)
药祭#氼 2024-07-16 05:52:07

该线程提供了很大的帮助,我将其包装到一个发送通知的 UIWindow 子类中。 我选择通知是为了使其成为真正的松散耦合,但您可以轻松添加委托。

要点如下:

http://gist.github.com/365998

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

This thread was a great help, and I wrapped it up into a UIWindow subclass that sends out notifications. I chose notifications to make it a real loose coupling, but you can add a delegate easily enough.

Here's the gist:

http://gist.github.com/365998

Also, the reason for the UIApplication subclass issue is that the NIB is setup to then create 2 UIApplication objects since it contains the application and the delegate. UIWindow subclass works great though.

变身佩奇 2024-07-16 05:52:07

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

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

在您的应用程序委托的 did finish launch 方法中,只需调用 addGesture 即可完成设置。 所有触摸都将通过 CatchAllGesture 的方法,而不会妨碍其他功能。

There's a way to do this app wide without individual controllers having to do anything. Just add a gesture recognizer that doesn't cancel touches. This way, all touches will be tracked for the timer, and other touches and gestures aren't affected at all so no one else has to know about it.

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

In your app delegate's did finish launch method, just call addGesture and you're all set. All touches will go through the CatchAllGesture's methods without it preventing the functionality of others.

风吹短裙飘 2024-07-16 05:52:07

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

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

等瞧!

Actually the subclassing idea works great. Just don't make your delegate the UIApplication subclass. Create another file that inherits from UIApplication (e.g. myApp). In IB set the class of the fileOwner object to myApp and in myApp.m implement the sendEvent method as above. In main.m do:

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

et voilà!

瞳孔里扚悲伤 2024-07-16 05:52:07

我刚刚在一个由动作控制的游戏中遇到了这个问题,即禁用了屏幕锁定,但在菜单模式下应该再次启用它。 我将所有对 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 的代码 iPhone 游戏中的空闲定时器处理

I just ran into this problem with a game that is controlled by motions i.e. has screen lock disabled but should enable it again when in menu mode. Instead of a timer I encapsulated all calls to setIdleTimerDisabled within a small class providing the following methods:

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

小梨窩很甜 2024-07-16 05:52:07

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

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

Here is another way to detect activity:

The timer is added in UITrackingRunLoopMode, so it can only fire if there is UITracking activity. It also has the nice advantage of not spamming you for all touch events, thus informing if there was activity in the last ACTIVITY_DETECT_TIMER_RESOLUTION seconds. I named the selector keepAlive as it seems an appropriate use case for this. You can of course do whatever you desire with the information that there was activity recently.

_touchesTimer = [NSTimer timerWithTimeInterval:ACTIVITY_DETECT_TIMER_RESOLUTION
                                        target:self
                                      selector:@selector(keepAlive)
                                      userInfo:nil
                                       repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_touchesTimer forMode:UITrackingRunLoopMode];
萌无敌 2024-07-16 05:52:07

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

Ultimately you need to define what you consider to be idle - is idle the result of the user not touching the screen or is it the state of the system if no computing resources are being used? It is possible, in many applications, for the user to be doing something even if not actively interacting with the device through the touch screen. While the user is probably familiar with the concept of the device going to sleep and the notice that it will happen via screen dimming, it is not necessarily the case that they'll expect something to happen if they are idle - you need to be careful about what you would do. But going back to the original statement - if you consider the 1st case to be your definition, there is no really easy way to do this. You'd need to receive each touch event, passing it along on the responder chain as needed while noting the time it was received. That will give you some basis for making the idle calculation. If you consider the second case to be your definition, you can play with an NSPostWhenIdle notification to try and perform your logic at that time.

终止放荡 2024-07-16 05:52:07

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

Outside is 2021 and I would like share my approach to handle this without extending the UIApplication. I will not describe how to create a timer and reset it. But rather how to catch all events. So your AppDelegate starts with this:

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

So all you need to do is to subclass UIWindow and override sendEvent, like below

import UIKit

class MyWindow: UIWindow {

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

And later create window with our class:

self.window = MyWindow(frame: UIScreen.main.bounds)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文