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


152

有没有人实施过一项功能,如果用户在一定时间内没有触摸屏幕,您会采取某种措施?我正在尝试找出最佳方法。

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

[UIApplication sharedApplication].idleTimerDisabled;

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

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

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

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


这是一个很好的问题。Windows具有OnIdle事件的概念,但我认为更多是因为该应用程序当前不处理其消息泵中的任何内容,而与iOS idleTimerDisabled属性似乎只与锁定设备有关。有人知道iOS / MacOSX中甚至没有什么东西与Windows概念遥不可及吗?
stonedauwg

Answers:


153

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

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

3
嗨,迈克,我的AppDelegate从NSObject继承而来,因此将其更改为UIApplication并实施上述方法以检测用户变得空闲,但是我收到错误消息“由于未捕获的异常'NSInternalInconsistencyException'而终止应用程序,原因:'只能有一个UIApplication实例。' “ ..还有什么我需要做的吗...?
Mihir Mehta 2010年

7
我要补充一点,UIApplication子类应该与UIApplicationDelegate子类分开
boliva 2012年

我不确定在计时器停止触发时设备进入非活动状态时如何使用?
anonmys 2012年

如果我为超时事件分配使用popToRootViewController函数,将无法正常工作。当我显示UIAlertView,然后显示popToRootViewController,然后用已弹出的
uiviewController中

4
非常好!但是,NSTimer如果有很多接触,这种方法会创建很多实例。
Andreas Ley 2012年

86

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

(为简洁起见,不包含内存清理代码。)


1
很好。这个答案真令人震惊!尽管我知道答案早于很多,但仍将答案标记为正确,但这现在是一个更好的解决方案。
Chintan Patel

很好,但是我发现了一个问题:在UITableViews中滚动不会导致调用nextResponder。我也尝试通过touchesBegan:和touchesMoved:进行跟踪,但是没有任何改善。有任何想法吗?
Greg Maletic

3
@GregMaletic:我遇到了同样的问题,但是最后我添加了-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {NSLog(@“ Will begin dragging”); }-(void)scrollViewDidScroll:(UIScrollView *)scrollView {NSLog(@“ Did Scroll”); [self resetIdleTimer]; }您是否尝试过?
Akshay Aher

谢谢。这仍然有帮助。我将其移植到Swift,效果很好。
Mark.ewd 2014年

你是摇滚明星。荣誉
Pras

21

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

2
我不明白为什么我们需要检查if idleTimer != nilsendEvent()方法是什么?
王光裕

如何timeoutInSeconds从Web服务响应中设置值?
User_1191

12

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

这是要点:

http://gist.github.com/365998

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


1
您能告诉我如何使用您的代码吗?我不知道该如何称呼
R. Dewi

2
它非常适合触摸,但似乎无法处理键盘输入。这意味着如果用户在gui键盘上输入内容,它将超时。
马丁·威克曼

2
我也无法理解如何使用它...我在我的视图控制器中添加了观察者,并期望在未触摸/空闲应用程序时触发通知b。但是什么也没发生...再加上我们可以控制空闲时间?就像我想要120秒的空闲时间,以便在120秒之后触发IdleNotification,而不是在此之前。
2013年

5

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

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

等等!


1
是的,创建一个独立的UIApplication子类似乎可以正常工作。我将第二个参数保留为零。
热舔

@Roby,看看我的查询stackoverflow.com/questions/20088521/…
2013年

4

我刚遇到一个由动作控制的游戏,即禁用了屏幕锁定功能,但在菜单模式下应再次启用它。我没有使用计时器,而是将所有对的调用封装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];
}

disableIdleTimerenableIdleTimerDelayed当进入菜单或在空闲计时器处于活动状态时应运行的任何东西时,它会停用空闲计时器,并enableIdleTimer从您的AppDelegate的applicationWillResignActive方法中调用该方法,以确保将所有更改正确地重置为系统默认行为。
我写了一篇文章,并提供了iPhone游戏中单例类IdleTimerManager 空闲计时器处理的代码。


4

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

计时器已添加到中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”选择器来满足您的任何需求。也许我想念您的观点?
Mihai Timar 2015年

您说这是检测活动的另一种方法,但是,这仅实例化了一个NSTimer的iVar。我不知道这如何回答OP的问题。
2015年

1
计时器已添加到UITrackingRunLoopMode中,因此仅在有UITracking活动时才可以触发。它还具有不对所有触摸事件发送垃圾邮件的优点,因此可以通知您过去ACTIVITY_DETECT_TIMER_RESOLUTION秒是否有活动。我将选择器命名为keepAlive,因为这似乎是一个合适的用例。当然,您可以根据最近发生的活动来做任何您想做的事情。
Mihai Timar,2015年

1
我想改善这个答案。如果您可以通过帮助使它变得更清晰,那将是一个很大的帮助。
米海·蒂玛

我将您的解释添加到您的答案中。现在,它变得更加有意义。
Jasper 2015年

3

最终,您需要定义您认为处于空闲状态的状态-是由于用户未触摸屏幕而导致的空闲状态,还是由于未使用任何计算资源而导致的系统状态?在许多应用中,即使用户没有通过触摸屏主动与设备进行交互,也有可能在做某事。尽管用户可能熟悉设备进入睡眠状态的概念以及通过屏幕变暗会注意到设备休眠的注意事项,但并不一定非要他们在闲置时会期望发生某些事情-您需要小心关于你会做什么。但是回到原始语句-如果您认为第一种情况是您的定义,则没有真正简单的方法可以做到这一点。您需要接收每个触摸事件,在需要时将其传递给响应者链,同时注意接收时间。这将为您进行空闲计算提供一些基础。如果您认为第二种情况是您的定义,则可以使用NSPostWhenIdle通知尝试尝试执行当时的逻辑。


1
为了澄清起见,我正在谈论与屏幕的交互。我将更新问题以反映这一点。
Mike McMaster

1
然后,您可以在任何时候发生触摸的情况下执行一些操作,以更新您检查的值,甚至设置(并重置)空闲计时器以触发,但是您需要自己实现,因为正如quarquark所说,构成空闲的原因在不同的应用程序。
Louis Gerbarg

1
我将“空闲”严格定义为自上次触摸屏幕以来的时间。我知道我需要自己实现它,我只是想知道截屏触摸的“最佳”方式是什么,或者是否有人知道确定此方法的另一种方法。
Mike McMaster

3

有一种方法可以广泛使用此应用程序,而无需单个控制器执行任何操作。只需添加不会取消触摸的手势识别器即可。这样,将为计时器跟踪所有触摸,而其他触摸和手势完全不受影响,因此其他人也无需知道。

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


1
我喜欢这种方法,将它用于Xamarin的类似问题:stackoverflow.com/a/51727021/250164
Wolfgang Schreurs,

1
效果很好,似乎该技术也用于控制AVPlayerViewController(私有API ref)中的UI控件的可见性。覆盖应用程序-sendEvent:是多余的,UITrackingRunLoopMode不能处理很多情况。
罗曼B.18年

@RomanB。是的。当你与iOS工作足够长的时间,你知道总是使用“正确的方式”,这是实现自定义手势的意图直截了当的方式developer.apple.com/documentation/uikit/uigesturerecognizer/...
Jlam

在touchesEnded中将状态设置为.failed会完成什么?
stonedauwg,

我喜欢这种方法,但是尝试时似乎只能抓住水龙头,而不能触摸触摸,平移,滑动等任何其他手势。那是故意的吗?
stonedauwg,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.