web-dev-qa-db-ja.com

iOSは、非アクティブ期間の後にアクションを実行します(ユーザーの操作なし)

ユーザーインタラクション(またはその欠如)に基づいてiOSアプリにタイマーを追加するにはどうすればよいですか?言い換えると、2分間ユーザーの操作がない場合、アプリに何かを実行させたいと思います。この場合、最初のView Controllerに移動します。 1:55に誰かが画面に触れると、タイマーがリセットされます。これはグローバルタイマーである必要があると思うので、どのビューを表示していても、インタラクションがないとタイマーが起動します。ただし、各ビューに独自のタイマーを作成できます。これまでに行われた提案、リンク、またはサンプルコードはありますか?

55
BobbyScon

アンが提供したリンクは素晴らしい出発点でしたが、私はn00bなので、既存のプロジェクトに翻訳することは困難でした。ブログ[元のブログはもう存在しません]を見つけましたが、これは段階的に改善されていますが、XCode 4.2用に作成されておらず、ストーリーボードを使用していません。以下は、アプリで動作するように非アクティブタイマーを取得した方法をまとめたものです。

  1. 新しいファイルを作成-> Objective-Cクラス->名前を入力し(私の場合はTIMERUIApplication)、サブクラスをUIApplicationに変更します。サブクラスフィールドに手動で入力する必要がある場合があります。これで、適切な.hおよび.mファイルが作成されました。

  2. .hファイルを次のように変更します。

    #import <Foundation/Foundation.h>
    
    //the length of time before your application "times out". This number actually represents seconds, so we'll have to multiple it by 60 in the .m file
    #define kApplicationTimeoutInMinutes 5
    
    //the notification your AppDelegate needs to watch for in order to know that it has indeed "timed out"
    #define kApplicationDidTimeoutNotification @"AppTimeOut"
    
    @interface TIMERUIApplication : UIApplication
    {
        NSTimer     *myidleTimer;
    }
    
    -(void)resetIdleTimer;
    
    @end
    
  3. .mファイルを次のように変更します。

    #import "TIMERUIApplication.h"
    
    @implementation TIMERUIApplication
    
    //here we are listening for any touch. If the screen receives touch, the timer is reset
    -(void)sendEvent:(UIEvent *)event
    {
        [super sendEvent:event];
    
        if (!myidleTimer)
        {
            [self resetIdleTimer];
        }
    
        NSSet *allTouches = [event allTouches];
        if ([allTouches count] > 0)
        {
            UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;
            if (phase == UITouchPhaseBegan)
            {
                [self resetIdleTimer];
            }
    
        }
    }
    //as labeled...reset the timer
    -(void)resetIdleTimer
    {
        if (myidleTimer)
        {
            [myidleTimer invalidate];
        }
        //convert the wait period into minutes rather than seconds
        int timeout = kApplicationTimeoutInMinutes * 60;
        myidleTimer = [NSTimer scheduledTimerWithTimeInterval:timeout target:self selector:@selector(idleTimerExceeded) userInfo:nil repeats:NO];
    
    }
    //if the timer reaches the limit as defined in kApplicationTimeoutInMinutes, post this notification
    -(void)idleTimerExceeded
    {
        [[NSNotificationCenter defaultCenter] postNotificationName:kApplicationDidTimeoutNotification object:nil];
    }
    
    
    @end
    
  4. Supporting Filesフォルダーに移動し、main.mを次のように変更します(以前のバージョンのXCodeとは異なります)。

    #import <UIKit/UIKit.h>
    
    #import "AppDelegate.h"
    #import "TIMERUIApplication.h"
    
    int main(int argc, char *argv[])
    {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, NSStringFromClass([TIMERUIApplication class]), NSStringFromClass([AppDelegate class]));
        }
    }
    
  5. AppDelegate.mファイルに残りのコードを記述します。このプロセスに関係のないコードは省略しました。 .hファイルに加える変更はありません。

    #import "AppDelegate.h"
    #import "TIMERUIApplication.h"
    
    @implementation AppDelegate
    
    @synthesize window = _window;
    
    -(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
    {      
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidTimeout:) name:kApplicationDidTimeoutNotification object:nil];
    
        return YES;
    }
    
    -(void)applicationDidTimeout:(NSNotification *) notif
    {
        NSLog (@"time exceeded!!");
    
    //This is where storyboarding vs xib files comes in. Whichever view controller you want to revert back to, on your storyboard, make sure it is given the identifier that matches the following code. In my case, "mainView". My storyboard file is called MainStoryboard.storyboard, so make sure your file name matches the storyboardWithName property.
        UIViewController *controller = [[UIStoryboard storyboardWithName:@"MainStoryboard" bundle:NULL] instantiateViewControllerWithIdentifier:@"mainView"];
    
        [(UINavigationController *)self.window.rootViewController pushViewController:controller animated:YES];
    }
    

注:タッチが検出されると、タイマーが開始されます。つまり、ユーザーがそのビューから移動せずにメイン画面(私の場合は「mainView」)にタッチすると、割り当てられた時間後に同じビューがプッシュされます。私のアプリにとっては大したことではありませんが、あなたにとってはそうかもしれません。タイマーは、タッチが認識されるとリセットされます。目的のページに戻ったらすぐにタイマーをリセットするには、... pushViewController:controller animated:YES];の後にこのコードを含めます。

[(TIMERUIApplication *)[UIApplication sharedApplication] resetIdleTimer];

これにより、インタラクションなしでただそこに座っている場合、ビューはx分ごとにプッシュされます。タイマーは、タッチを認識するたびにリセットされるため、引き続き機能します。

特に「mainView」が現在表示されている場合にタイマーを無効にするなど、改善を提案した場合はコメントしてください。現在のビューを登録するためにifステートメントを取得するためのifステートメントを理解できないようです。しかし、私は自分がいる場所に満足しています。以下は、ifステートメントでの最初の試みです。これにより、私がどこに向かっていたかを確認できます。

-(void)applicationDidTimeout:(NSNotification *) notif
{
    NSLog (@"time exceeded!!");
    UIViewController *controller = [[UIStoryboard storyboardWithName:@"MainStoryboard" bundle:NULL] instantiateViewControllerWithIdentifier:@"mainView"];

    //I've tried a few varieties of the if statement to no avail. Always goes to else.
    if ([controller isViewLoaded]) {
        NSLog(@"Already there!");
    }
    else {
        NSLog(@"go home");
        [(UINavigationController *)self.window.rootViewController pushViewController:controller animated:YES];
        //[(TIMERUIApplication *)[UIApplication sharedApplication] resetIdleTimer];
    }
}

私はまだn00bであり、すべてを最善の方法で行っていないかもしれません。提案はいつでも歓迎します。

116
BobbyScon

ボビーが提案したものを実装しましたが、Swiftで。コードの概要は次のとおりです。

  1. 新しいファイルを作成-> Swift File->名前(私の場合はTimerUIApplication)を入力し、サブクラスをUIApplicationに変更します。TimerUIApplication.Swiftファイルを次のように変更します。

    class TimerUIApplication: UIApplication {
    
        static let ApplicationDidTimoutNotification = "AppTimout"
    
        // The timeout in seconds for when to fire the idle timer.
        let timeoutInSeconds: TimeInterval = 5 * 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 event.allTouches?.first(where: { $0.phase == .began }) != nil {
                resetIdleTimer()
            }
        }
    
        // Resent the timer because there was user interaction.
        func resetIdleTimer() {
            idleTimer?.invalidate()
            idleTimer = Timer.scheduledTimer(timeInterval: timeoutInSeconds, target: self, selector: #selector(AppDelegate.idleTimerExceeded), userInfo: nil, repeats: false)
        }
    
        // If the timer reaches the limit as defined in timeoutInSeconds, post this notification.
        func idleTimerExceeded() {
            Foundation.NotificationCenter.default.post(name: NSNotification.Name(rawValue: TimerUIApplication.ApplicationDidTimoutNotification), object: nil)
        }
    }
    
  2. 新しいファイルを作成します-> Swift File-> main.Swift(名前は重要です)。

    import UIKit
    
    UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(TimerUIApplication), NSStringFromClass(AppDelegate))
    
  3. AppDelegateで:Remove @UIApplicationMain AppDelegateの上。

    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
            NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(AppDelegate.applicationDidTimout(_:)), name: TimerUIApplication.ApplicationDidTimoutNotification, object: nil)
            return true
        }
    
        ...
    
        // The callback for when the timeout was fired.
        func applicationDidTimout(notification: NSNotification) {
            if let vc = self.window?.rootViewController as? UINavigationController {
                if let myTableViewController = vc.visibleViewController as? MyMainViewController {
                    // Call a function defined in your view controller.
                    myMainViewController.userIdle()
                } else {
                  // We are not on the main view controller. Here, you could segue to the desired class.
                  let storyboard = UIStoryboard(name: "MyStoryboard", bundle: nil)
                  let vc = storyboard.instantiateViewControllerWithIdentifier("myStoryboardIdentifier")
                }
            }
        }
    }
    

ルートView Controllerに応じてapplicationDidTimoutで異なることを行う必要がある場合があることに注意してください。 View Controllerのキャスト方法の詳細については、 この投稿 を参照してください。 Navigation Controller上にモーダルビューがある場合、 topViewController の代わりに visibleViewController を使用できます。

23
Vanessa Forney

背景[Swift Solution]

Swift=でこの回答を更新する要求があったため、以下にスニペットを追加しました。

私は自分自身の使用のために仕様を多少変更していることに注意してください:UIEventsが5秒間ない場合、基本的に作業をしたいです。着信UIEventは、以前のタイマーをキャンセルし、新しいタイマーで再起動します。

上記の回答との違い

  • accepted answer 上記からの変更:最初のイベントで最初のタイマーを設定する代わりに、init()ですぐにタイマーを設定します。また、私のreset_idle_timer()は前のタイマーをキャンセルするため、常に1つのタイマーのみが実行されます。

重要:構築前の2つのステップ

SOに関するいくつかの素晴らしい回答のおかげで、上記のコードをSwift code。

  • SwiftでUIApplicationをサブクラス化する方法の概要については、 この回答 に従ってください。 Swiftまたは以下のスニペットはコンパイルされません。リンクされた答えはステップを非常によく説明しているので、ここでは繰り返しません。1分もかからないはずです。正しく読み、設定します。

  • NSTimerの_cancelPreviousPerformRequestsWithTarget:_を動作させることができなかったので、これを見つけました 更新されたGCDソリューション これは素晴らしい動作です。そのコードを別の.Swiftファイルにドロップするだけで、gtgになります(したがって、delay()およびcancel_delay()を呼び出して、_dispatch_cancelable_closure_を使用できます)。

私見、以下のコードは誰でも理解できるほど単純です。この回答に関する質問に回答していないことを事前におaび申し上げます(仕事の気圧が少しあふれています)。

SOどんな素晴らしい情報が得られたかに貢献するためにこの回答を投稿しました。

スニペット

_import UIKit
import Foundation

private let g_secs = 5.0

class MYApplication: UIApplication
{
    var idle_timer : dispatch_cancelable_closure?

    override init()
    {
        super.init()
        reset_idle_timer()
    }

    override func sendEvent( event: UIEvent )
    {
        super.sendEvent( event )

        if let all_touches = event.allTouches() {
            if ( all_touches.count > 0 ) {
                let phase = (all_touches.anyObject() as UITouch).phase
                if phase == UITouchPhase.Began {
                    reset_idle_timer()
                }
            }
        }
    }

    private func reset_idle_timer()
    {
        cancel_delay( idle_timer )
        idle_timer = delay( g_secs ) { self.idle_timer_exceeded() }
    }

    func idle_timer_exceeded()
    {
        println( "Ring ----------------------- Do some Idle Work!" )
        reset_idle_timer()
    }
}
_
14
kfmfe04

Swift 3の例はこちら

  1. のようなクラスを作成します。

     import Foundation
     import UIKit
    
     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//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)
       // print("3")
      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 {
        // print("1")
         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() {
          print("Time Out")
    
       NotificationCenter.default.post(name:Notification.Name.TimeOutUserInteraction, object: nil)
    
         //Go Main page after 15 second
    
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
       appDelegate.window = UIWindow(frame: UIScreen.main.bounds)
        let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
       let yourVC = mainStoryboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
      appDelegate.window?.rootViewController = yourVC
      appDelegate.window?.makeKeyAndVisible()
    
    
       }
    }
    
  2. main.Swiftという名前の別のクラスを作成します

    import Foundation
       import UIKit
    
       CommandLine.unsafeArgv.withMemoryRebound(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc))
        {    argv in
                _ = UIApplicationMain(CommandLine.argc, argv, NSStringFromClass(InterractionUIApplication.self), NSStringFromClass(AppDelegate.self))
            }
    
  3. appDelegateから@ UIApplicationMainを削除することを忘れないでください

  4. Swift 3の完全なソースコードがGitHubに提供されます。 GitHubリンク: https://github.com/enamul95/UserInactivity

4
Enamul Haque

注:タッチが検出されると、タイマーが開始されます。つまり、ユーザーがそのビューから移動せずにメイン画面(私の場合は「mainView」)にタッチすると、割り当てられた時間後に同じビューがプッシュされます。私のアプリにとっては大したことではありませんが、あなたにとってはそうかもしれません。タイマーは、タッチが認識されるとリセットされます。目的のページに戻ったらすぐにタイマーをリセットするには、... pushViewController:controller animated:YES];の後にこのコードを含めます。

同じビューが再び表示されるというこの問題に対する解決策の1つは、appleelegateにBOOLを設定し、ユーザーがアイドル状態になっていることを確認するときにtrueに設定し、アイドルビューに移動したときにfalseに設定することです。次に、idleTimerExceededメソッドのTIMERUIApplicationに、次のようなifステートメントがあります。アイドル状態になっているユーザーを確認するすべてのビューのviewDidloadビューで、appdelegate.idleをtrueに設定します。アイドル状態のユーザーを確認する必要がない他のビューがある場合、これをfalseに設定できます。 。

-(void)idleTimerExceeded{
          AppDelegate *appdelegate = [[UIApplication sharedApplication] delegate];

          if(appdelegate.idle){
            [[NSNotificationCenter defaultCenter] postNotificationName: kApplicationDidTimeOutNotification object:nil]; 
          }
}
4
David

Vanessa's Answerのサブクラス化されたUIApplicationのSwift 3.0変換

class TimerUIApplication: UIApplication {
static let ApplicationDidTimoutNotification = "AppTimout"

    // The timeout in seconds for when to fire the idle timer.
    let timeoutInSeconds: TimeInterval = 5 * 60

    var idleTimer: Timer?

    // 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(TimerUIApplication.idleTimerExceeded), userInfo: nil, repeats: false)
    }

    // If the timer reaches the limit as defined in timeoutInSeconds, post this notification.
    func idleTimerExceeded() {
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: TimerUIApplication.ApplicationDidTimoutNotification), object: nil)
    }


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

    }
}
2
Alan