web-dev-qa-db-fra.com

iOS effectue une action après une période d'inactivité (aucune interaction avec l'utilisateur)

Comment puis-je ajouter une minuterie à mon application iOS basée sur l'interaction de l'utilisateur (ou son absence)? En d'autres termes, s'il n'y a pas d'interaction utilisateur pendant 2 minutes, je veux que l'application fasse quelque chose, dans ce cas, accédez au contrôleur de vue initial. Si à 1 h 55 quelqu'un touche l'écran, le chronomètre se réinitialise. Je pense que cela devrait être une minuterie globale, donc peu importe la vue sur laquelle vous vous trouvez, le manque d'interaction démarre la minuterie. Cependant, je pouvais créer une minuterie unique sur chaque vue. Quelqu'un at-il des suggestions, des liens ou des exemples de code où cela a déjà été fait?

55
BobbyScon

Le lien fourni par Anne était un excellent point de départ, mais, étant le n00b que je suis, il était difficile de le traduire dans mon projet existant. J'ai trouvé un blog [le blog d'origine n'existe plus] qui donnait une meilleure étape par étape, mais il n'a pas été écrit pour XCode 4.2 et en utilisant des storyboards. Voici une description de la façon dont j'ai fait fonctionner le minuteur d'inactivité pour mon application:

  1. Créez un nouveau fichier -> classe Objective-C -> tapez un nom (dans mon cas TIMERUIApplication) et changez la sous-classe en UIApplication. Vous devrez peut-être saisir ceci manuellement dans le champ de la sous-classe. Vous devriez maintenant avoir les fichiers .h et .m appropriés.

  2. Modifiez le fichier .h pour lire comme suit:

    #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. Modifiez le fichier .m pour lire comme suit:

    #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. Allez dans votre dossier Supporting Files et modifiez main.m en ceci (différent des versions précédentes de 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. Écrivez le code restant dans votre fichier AppDelegate.m. J'ai omis du code ne concernant pas ce processus. Il n'y a aucune modification à apporter dans le fichier .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];
    }
    

Remarques: La minuterie démarre chaque fois qu'une touche est détectée. Cela signifie que si l'utilisateur touche l'écran principal (dans mon cas "mainView") même sans s'éloigner de cette vue, la même vue se repoussera après le temps imparti. Ce n'est pas grave pour mon application, mais pour la vôtre, cela pourrait l'être. La minuterie ne se réinitialise qu'une fois qu'un contact est reconnu. Si vous voulez réinitialiser la minuterie dès que vous revenez à la page où vous voulez être, incluez ce code après le ... pushViewController: contrôleur animé: OUI];

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

Cela entraînera la vue à pousser toutes les x minutes si elle est juste assise là sans interaction. La minuterie sera toujours réinitialisée chaque fois qu'elle reconnaît un toucher, ce qui fonctionnera toujours.

Veuillez commenter si vous avez suggéré des améliorations, en particulier pour désactiver le minuteur si le "mainView" est actuellement affiché. Je n'arrive pas à comprendre ma déclaration if pour qu'elle enregistre la vue actuelle. Mais je suis content de l'endroit où j'en suis. Ci-dessous est ma tentative initiale de l'instruction if afin que vous puissiez voir où j'allais avec.

-(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];
    }
}

Je suis toujours un n00b et je n'ai peut-être pas tout fait de la meilleure façon. Les suggestions sont toujours les bienvenues.

116
BobbyScon

J'ai mis en œuvre ce que Bobby a suggéré, mais dans Swift. Le code est décrit ci-dessous.

  1. Créez un nouveau fichier -> Swift File -> tapez un nom (dans mon cas TimerUIApplication) et changez la sous-classe en UIApplication. Modifiez le fichier TimerUIApplication.Swift pour lire comme suit:

    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. Créez un nouveau fichier -> Swift File -> main.Swift (le nom est important).

    import UIKit
    
    UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(TimerUIApplication), NSStringFromClass(AppDelegate))
    
  3. Dans votre AppDelegate: Supprimer @UIApplicationMain au-dessus de l'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")
                }
            }
        }
    }
    

Gardez à l'esprit que vous devrez peut-être faire différentes choses dans applicationDidTimout en fonction de votre contrôleur de vue racine. Voir cet article pour plus de détails sur la façon dont vous devez caster votre contrôleur de vue. Si vous avez des vues modales sur le contrôleur de navigation, vous pouvez utiliser visibleViewController au lieu de topViewController .

23
Vanessa Forney

Contexte [Solution rapide]

Il y avait une demande de mise à jour de cette réponse avec Swift donc j'ai ajouté un extrait ci-dessous.

Notez que j'ai quelque peu modifié les spécifications pour mes propres utilisations: je veux essentiellement faire du travail s'il n'y a pas de UIEvents pendant 5 secondes. Toute touche entrante UIEvent annulera les minuteurs précédents et redémarrera avec un nouveau minuteur.

Différences par rapport à la réponse ci-dessus

  • Quelques changements par rapport à réponse acceptée ci-dessus: au lieu de configurer le premier temporisateur lors du premier événement, je configure immédiatement mon temporisateur dans init(). De plus, ma reset_idle_timer() annulera la minuterie précédente, donc une seule minuterie sera exécutée à tout moment.

IMPORTANT: 2 étapes avant de construire

Grâce à quelques bonnes réponses sur SO, j'ai pu adapter le code ci-dessus en tant que code Swift.

  • Suivez cette réponse pour un aperçu de la façon de sous-classer UIApplication dans Swift. Assurez-vous de suivre ces étapes pour Swift ou l'extrait ci-dessous ne sera pas compilé. Étant donné que la réponse liée décrit si bien les étapes, je ne répéterai pas ici. Cela devrait vous prendre moins d'une minute pour le lire et le configurer correctement.

  • Je n'ai pas pu faire fonctionner le cancelPreviousPerformRequestsWithTarget: De NSTimer, j'ai donc trouvé cela mise à jour de la solution GCD qui fonctionne très bien. Déposez simplement ce code dans un fichier .Swift séparé et vous êtes gtg (vous pouvez donc appeler delay() et cancel_delay(), et utiliser dispatch_cancelable_closure).

À mon humble avis, le code ci-dessous est assez simple pour que quiconque puisse le comprendre. Je m'excuse à l'avance de ne pas avoir répondu à toutes les questions sur cette réponse (un peu inondée d'atmosphère de travail).

Je viens de poster cette réponse pour contribuer à SO quelles bonnes informations j'ai obtenues.

Extrait

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

Exemple de Swift 3 ici

  1. créer une classe comme.

     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. créer une autre classe nommée main.Swift coller le code ci-dessous

    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. n'oubliez pas de supprimer @ UIApplicationMain d'AppDelegate

  4. Le code source complet de Swift 3 est donné à GitHub. Lien GitHub: https://github.com/enamul95/UserInactivity

4
Enamul Haque

Remarques: La minuterie démarre chaque fois qu'une touche est détectée. Cela signifie que si l'utilisateur touche l'écran principal (dans mon cas "mainView") même sans s'éloigner de cette vue, la même vue se repoussera après le temps imparti. Ce n'est pas grave pour mon application, mais pour la vôtre, cela pourrait l'être. La minuterie ne se réinitialise qu'une fois qu'un contact est reconnu. Si vous voulez réinitialiser la minuterie dès que vous revenez à la page où vous voulez être, incluez ce code après le ... pushViewController: contrôleur animé: OUI];

Une solution à ce problème de la même vue recommençant affichée est d'avoir un BOOL dans l'appdelegate et de le définir sur true lorsque vous voulez vérifier que l'utilisateur est inactif et de le définir sur false lorsque vous êtes passé à la vue inactive. Ensuite, dans la TIMERUIApplication de la méthode idleTimerExceeded, ayez une instruction if comme ci-dessous. Dans la vue viewDidload de toutes les vues où vous voulez vérifier que l'utilisateur commence inactif, vous définissez appdelegate.idle sur true, s'il existe d'autres vues où vous n'avez pas besoin de vérifier que l'utilisateur est inactif, vous pouvez définir ceci sur false .

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

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

Swift 3.0 Conversion du UIApplication sous-classé dans la réponse de Vanessa

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