web-dev-qa-db-ja.com

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

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

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

[UIApplication sharedApplication].idleTimerDisabled;

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

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

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

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

150
Mike McMaster

私が探していた答えは次のとおりです。

アプリケーションにサブクラス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を変更して、デリゲートクラス(この例ではAppDelegate)をプリンシパルクラスとして使用するようにUIApplicationMainに指示する必要もあります。

int retVal = UIApplicationMain(argc, argv, @"AppDelegate", @"AppDelegate");
152
Mike McMaster

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

また、アイドルタイマーがリセットされるたびに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

(簡潔にするため、メモリクリーンアップコードは除外されています。)

86
Chris Miles

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)
18
Sergey Stadnik

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

ここに要点があります:

http://Gist.github.com/365998

また、UIApplicationサブクラスの問題の理由は、NIBがアプリケーションとデリゲートを含むため、2つのUIApplicationオブジェクトを作成するようにセットアップされていることです。ただし、UIWindowサブクラスは素晴らしい働きをします。

12
Brian King

アクティビティを検出する別の方法を次に示します。

タイマーは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];
4
Mihai Timar

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

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

ほら!

4
Roby

私はちょうどモーションで制御されているゲームでこの問題に遭遇しました。つまり、画面ロックが無効になっていますが、メニューモードのときに再び有効にする必要があります。タイマーの代わりに、次のメソッドを提供する小さなクラス内で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のenableIdleTimerメソッドからapplicationWillResignActiveメソッドを呼び出して、すべての変更をリセットしますシステムのデフォルトの動作に適切に。
記事を書き、シングルトンクラスIdleTimerManagerのコードを提供しました iPhoneゲームでのアイドルタイマー処理

4
Kay

個々のコントローラーが何もしなくても、このアプリ全体を実行する方法があります。タッチをキャンセルしないジェスチャレコグナイザーを追加するだけです。この方法では、タイマーのすべてのタッチが追跡され、他のタッチとジェスチャーはまったく影響を受けないため、他のユーザーはそのことを知る必要がありません。

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のメソッドを通過します。

3
Jlam

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

3
wisequark