iPhone:最後の画面タッチからのユーザーの非アクティブ/アイドル時間の検出


152

ユーザーが一定期間画面に触れなかった場合に、特定のアクションを実行する機能を実装した人はいますか?私はそれを行うための最良の方法を見つけ出そうとしています。

UIApplicationには、この多少関連するメソッドがあります。

[UIApplication sharedApplication].idleTimerDisabled;

代わりに次のようなものがあればいいでしょう:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

次に、タイマーを設定してこの値を定期的にチェックし、しきい値を超えたときに何らかのアクションを実行します。

うまくいけば、それが私が探しているものを説明しています。誰かがすでにこの問題に取り組んでいますか、またはあなたがそれをどのように行うかについて何か考えを持っていますか?ありがとう。


これは素晴らしい質問です。WindowsにはOnIdleイベントの概念がありますが、これは、アプリがメッセージポンプで現在何も処理していないことと、デバイスのロックのみに関係していると思われるiOSのidleTimerDisabledプロパティに関するものだと思います。iOS / MacOSXのWindowsコンセプトにリモートで近いものがあるかどうか誰でも知っていますか?
stonedauwg

回答:


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インスタンスは1つしか存在できません。」 「..他に必要なことは...?
Mihir Mehta

7
UIApplicationサブクラスはUIApplicationDelegateサブクラスから分離する必要があることを追加します
boliva

タイマーが作動を停止したときにデバイスが非アクティブ状態になると、これがどのように機能するかわかりませんか?
anonmys

タイムアウトイベントにpopToRootViewController関数の使用を割り当てた場合、正しく動作しません。私がUIAlertView、その後、popToRootViewControllerを表示するとき、私はすでにポップさのUIViewControllerからセレクタでUIAlertViewのいずれかのボタンを押す起こる
Gargo

4
非常に素晴らしい!ただし、このアプローチNSTimerは、多くのタッチがある場合、多くのインスタンスを作成します。
Andreas Ley

86

UIApplicationのサブクラス化を必要としないアイドルタイマーソリューションのバリエーションがあります。これは特定のUIViewControllerサブクラスで機能するため、(インタラクティブアプリやゲームのように)ビューコントローラーが1つしかない場合や、特定のビューコントローラーでのみアイドルタイムアウトを処理する場合に便利です。

また、アイドルタイマーがリセットされるたびに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
とても良い。この答えはすごい!正解としてマークされた回答を上回っていますが、かなり前にわかっていましたが、これは現在より良い解決策です。
チンタンパテル

これはすばらしいですが、UITableViewsでのスクロールによってnextResponderが呼び出されないという1つの問題が見つかりました。また、touchesBegan:とtouchesMoved:を使用して追跡を試みましたが、改善はありませんでした。何か案は?
Greg Maletic 2013年

3
@GregMaletic:同じ問題がありましたが、最後に追加しました-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {NSLog(@ "Will begin dragging"); }-(void)scrollViewDidScroll:(UIScrollView *)scrollView {NSLog(@ "Did Scroll"); [self resetIdleTimer]; }これを試しましたか?
Akshay Aher 2013

ありがとう。これはまだ役に立ちます。私はそれを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 != nilインsendEvent()方法が必要な理由がわかりません。
Guangyu王

timeoutInSecondsWebサービス応答からの値をどのように設定できますか?
User_1191

12

このスレッドは非常に役立ちました。通知を送信するUIWindowサブクラスにまとめました。私は通知を実際の疎結合にするために選択しましたが、デリゲートは簡単に追加できます。

ここに要点があります:

http://gist.github.com/365998

また、UIApplicationサブクラスの問題の理由は、アプリケーションとデリゲートが含まれているため、NIBが2つのUIApplicationオブジェクトを作成するように設定されていることです。しかし、UIWindowサブクラスはうまく機能します。


1
コードの使い方を教えてもらえますか?私はそれを呼び出す方法を理解してい
ませ

2
タッチには最適ですが、キーボードからの入力を処理しないようです。これは、ユーザーがGUIキーボードで何かを入力しているとタイムアウトになることを意味します。
マーティンウィックマン

2
私もそれを使用する方法を理解できません...私はビューコントローラーにオブザーバーを追加し、アプリがタッチされていない/アイドル状態のときに通知が発生することを期待しています..しかし、何も起こりませんでした...それに加えて、アイドル時間を制御できますか?120秒のアイドル時間を望んでいるので、120秒後にIdleNotificationがその前ではなく起動するようにします。
Ans

5

実際、サブクラス化のアイデアはうまく機能します。デリゲートをUIApplicationサブクラスにしないでください。から継承する別のファイルを作成しますUIApplication(例:myApp)。IBでfileOwnerオブジェクトのクラスをに設定し、myAppmyApp.mでsendEvent上記のようにメソッドを実装します。main.mで次のようにします。

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

etvoilà!


1
はい、スタンドアロンのUIApplicationサブクラスを作成すると問題なく動作するようです。私はメインに2番​​目のパラメーターを残しました。
ホットリックス

@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メソッドから呼び出されて、すべての変更がシステムのデフォルトの動作に正しくリセットされるようにします。
私は記事を書いて、シングルトンクラスIdleTimerManagerためのコードを提供するiPhoneゲームでの取り扱いアイドルタイマー


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

これはアクティビティを検出する別の方法であると言いますが、これはNSTimerであるiVarをインスタンス化するだけです。これがOPの質問にどのように答えるかはわかりません。
Jasper

1
タイマーはUITrackingRunLoopModeに追加されるため、UITrackingアクティビティがある場合にのみ起動できます。また、すべてのタッチイベントについてスパムを送信しないという優れた利点もあり、最後のACTIVITY_DETECT_TIMER_RESOLUTION秒にアクティビティがあったかどうかを通知します。これは適切なユースケースのように思われるため、セレクターにkeepAliveという名前を付けました。もちろん、最近活動があったという情報を使って、好きなことをすることができます。
Mihai Timar

1
この答えを改善したいと思います。あなたがそれをより明確にするための指針を助けることができるならば、それは大きな助けになるでしょう。
Mihai Timar

答えに説明をつけました。今ではもっと理にかなっています。
Jasper

3

最終的に、アイドルと見なすものを定義する必要があります。ユーザーが画面に触れていないためにアイドル状態になりますか、それともコンピューティングリソースが使用されていない場合のシステムの状態ですか。多くのアプリケーションでは、タッチスクリーンを介してデバイスとアクティブに対話していなくても、ユーザーが何かをしている可能性があります。ユーザーはおそらく、デバイスがスリープ状態になるという概念と、画面が暗くなることによって起こることに気づいていますが、アイドル状態のときに何かが起こると期待しているとは限りません-注意する必要がありますあなたがどうするかについて。ただし、元のステートメントに戻ると、最初のケースを自分の定義だと考える場合、これを行う簡単な方法はありません。それぞれのタッチイベントを受け取る必要があります。受信時刻を記録しながら、必要に応じてレスポンダーチェーンに渡します。これにより、アイドル計算を行うためのいくつかの基礎が得られます。2番目のケースを定義であると考える場合は、NSPostWhenIdle通知を試して、その時点でロジックを実行することができます。


1
明確にするために、私は画面との相互作用について話しています。それを反映するように質問を更新します。
マイクマクマスター

1
次に、タッチが発生するたびにチェックする値を更新するか、アイドルタイマーを起動するように設定(およびリセット)することもできますが、賢明なクォークが言ったように、アイドルを構成するものは異なるため、自分で実装する必要がありますさまざまなアプリ。
Louis Gerbarg、2008年

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参照)のUIコントロールの可視性を制御するために使用されるようです。アプリのオーバーライド-sendEvent:はやり過ぎであり、UITrackingRunLoopMode多くのケースを処理しません。
ローマン

@RomanB。そうだね。あなたは十分な長さあなたは常に「正しい方法」を使用することを知っていると、これは意図したとおりのカスタムジェスチャーを実装するための簡単な方法であるのiOSで働いてきたときdeveloper.apple.com/documentation/uikit/uigesturerecognizer/...
Jlam

状態を.failedに設定すると、touchesEndedで何が達成されますか?
stonedauwg

私はこのアプローチが好きですが、試してみるとタップをキャッチするだけで、リセットロジックが移動するtouchesEndedでパンやスワイプなどの他のジェスチャーはキャッチしません。それは意図されたのですか?
stonedauwg
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.