iPhone: détection de l'inactivité / du temps d'inactivité de l'utilisateur depuis la dernière touche d'écran

152

Quelqu'un a-t-il implémenté une fonctionnalité où si l'utilisateur n'a pas touché l'écran pendant une certaine période, vous prenez une certaine action? J'essaie de trouver la meilleure façon de le faire.

Il y a cette méthode quelque peu liée dans UIApplication:

[UIApplication sharedApplication].idleTimerDisabled;

Ce serait bien si vous aviez quelque chose comme ça à la place:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Ensuite, je pourrais configurer une minuterie et vérifier périodiquement cette valeur, et prendre des mesures lorsqu'elle dépasse un seuil.

J'espère que cela explique ce que je recherche. Quelqu'un a-t-il déjà abordé ce problème ou a-t-il des idées sur la façon dont vous le feriez? Merci.

Mike McMaster
la source
c'est une excellente question. Windows a le concept d'un événement OnIdle, mais je pense qu'il s'agit davantage de l'application qui ne gère actuellement rien dans sa pompe de message par rapport à la propriété iOS idleTimerDisabled qui ne semble concerner que le verrouillage de l'appareil. Quelqu'un sait-il s'il y a quelque chose, même à distance, proche du concept Windows dans iOS / MacOSX?
stonedauwg

Réponses:

153

Voici la réponse que je cherchais:

Demandez à votre délégué d'application de sous-classe UIApplication. Dans le fichier d'implémentation, remplacez la méthode sendEvent: comme suit:

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

où maxIdleTime et idleTimer sont des variables d'instance.

Pour que cela fonctionne, vous devez également modifier votre main.m pour indiquer à UIApplicationMain d'utiliser votre classe de délégué (dans cet exemple, AppDelegate) comme classe principale:

int retVal = UIApplicationMain(argc, argv, @"AppDelegate", @"AppDelegate");
Mike McMaster
la source
3
Bonjour Mike, Mon AppDelegate hérite de NSObject Donc l'a changé UIApplication et implémentez les méthodes ci-dessus pour détecter l'utilisateur devenant inactif, mais j'obtiens l'erreur "Terminer l'application en raison d'une exception non interceptée 'NSInternalInconsistencyException', raison: 'Il ne peut y avoir qu'une seule instance UIApplication.' ".. est-ce que je dois faire autre chose ...?
Mihir Mehta
7
J'ajouterais que la sous-classe UIApplication devrait être séparée de la sous
UIApplicationDelegate
Je ne sais pas comment cela fonctionnera avec l'appareil entrant dans l'état inactif lorsque les minuteries cessent de se déclencher?
anonmys
ne fonctionne pas correctement si j'attribue l'utilisation de la fonction popToRootViewController pour l'événement timeout. Cela se produit lorsque j'affiche UIAlertView, puis popToRootViewController, puis j'appuie sur n'importe quel bouton de UIAlertView avec un sélecteur de uiviewController qui est déjà
affiché
4
Très agréable! Cependant, cette approche crée de nombreuses NSTimerinstances s'il y a beaucoup de touches.
Andreas Ley
86

J'ai une variante de la solution de minuterie d'inactivité qui ne nécessite pas de sous-classement UIApplication. Il fonctionne sur une sous-classe UIViewController spécifique, il est donc utile si vous n'avez qu'un seul contrôleur de vue (comme une application ou un jeu interactif peut avoir) ou si vous souhaitez uniquement gérer le délai d'inactivité dans un contrôleur de vue spécifique.

Il ne recrée pas non plus l'objet NSTimer chaque fois que le minuteur d'inactivité est réinitialisé. Il n'en crée un nouveau que si la minuterie se déclenche.

Votre code peut appeler resetIdleTimerpour tout autre événement qui peut nécessiter d'invalider la minuterie d'inactivité (comme une entrée d'accéléromètre importante).

@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

(code de nettoyage de la mémoire exclu par souci de concision.)

Chris Miles
la source
1
Très bien. Cette réponse est géniale! Bat la réponse marquée comme correcte même si je sais que c'était beaucoup plus tôt, mais c'est une meilleure solution maintenant.
Chintan Patel
C'est génial, mais j'ai trouvé un problème: le défilement dans UITableViews ne provoque pas l'appel de nextResponder. J'ai également essayé le suivi via touchesBegan: et touchesMoved :, mais aucune amélioration. Des idées?
Greg Maletic
3
@GregMaletic: j'ai eu le même problème mais j'ai finalement ajouté - (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView {NSLog (@ "Will begin dragging"); } - (void) scrollViewDidScroll: (UIScrollView *) scrollView {NSLog (@ "Did Scroll"); [self resetIdleTimer]; } avez-vous essayé ça?
Akshay Aher
Merci. Ceci est toujours utile. Je l'ai porté sur Swift et cela a très bien fonctionné.
Mark.ewd
vous êtes une rockstar. kudos
Pras
21

Pour swift v 3.1

n'oubliez pas de commenter cette ligne dans 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)
   }
} 

créez le fichier main.swif et ajoutez-le (le nom est important)

CommandLine.unsafeArgv.withMemoryRebound(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)) {argv in
_ = UIApplicationMain(CommandLine.argc, argv, NSStringFromClass(InterractionUIApplication.self), NSStringFromClass(AppDelegate.self))
}

Observer la notification dans une autre classe

NotificationCenter.default.addObserver(self, selector: #selector(someFuncitonName), name: Notification.Name.TimeOutUserInteraction, object: nil)
Sergey Stadnik
la source
2
Je ne comprends pas pourquoi nous avons besoin d' un chèque if idleTimer != nilen sendEvent()méthode?
Guangyu Wang
Comment définir la valeur timeoutInSecondsde la réponse du service Web?
User_1191
12

Ce fil a été d'une grande aide, et je l'ai encapsulé dans une sous-classe UIWindow qui envoie des notifications. J'ai choisi les notifications pour en faire un véritable couplage lâche, mais vous pouvez ajouter un délégué assez facilement.

Voici l'essentiel:

http://gist.github.com/365998

En outre, la raison du problème de sous-classe UIApplication est que le NIB est configuré pour créer ensuite 2 objets UIApplication car il contient l'application et le délégué. La sous-classe UIWindow fonctionne très bien cependant.

Brian King
la source
1
pouvez-vous me dire comment utiliser votre code? Je ne comprends pas comment l'appeler
R. Dewi
2
Cela fonctionne très bien pour les touches, mais ne semble pas gérer les entrées du clavier. Cela signifie qu'il expirera si l'utilisateur tape des éléments sur le clavier de l'interface graphique.
Martin Wickman
2
Moi aussi, je ne suis pas en mesure de comprendre comment l'utiliser ... J'ajoute des observateurs dans mon contrôleur de vue et j'attends des notifications à b déclencher lorsque l'application est intacte / inactive .. mais rien ne s'est passé ... et d'où nous pouvons contrôler le temps d'inactivité? comme je veux un temps d'inactivité de 120 secondes pour qu'après 120 secondes IdleNotification se déclenche, pas avant cela.
Ans
5

En fait, l'idée de sous-classification fonctionne très bien. Ne faites pas de votre délégué la UIApplicationsous - classe. Créez un autre fichier qui hérite de UIApplication(par exemple myApp). Dans IB, définissez la classe de l' fileOwnerobjet sur myAppet dans myApp.m implémentez la sendEventméthode comme ci-dessus. Dans main.m faire:

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

et voilà!

Roby
la source
1
Oui, la création d'une sous-classe UIApplication autonome semble fonctionner correctement. J'ai laissé le deuxième parm nul en principal.
Hot Licks
@Roby, regarde ma requête stackoverflow.com/questions/20088521/… .
Tirth
4

Je viens de rencontrer ce problème avec un jeu qui est contrôlé par des mouvements, c'est-à-dire que le verrouillage de l'écran est désactivé mais devrait le réactiver en mode menu. Au lieu d'un minuteur, j'ai encapsulé tous les appels setIdleTimerDisableddans une petite classe fournissant les méthodes suivantes:

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

disableIdleTimerdésactive la minuterie d'inactivité, enableIdleTimerDelayedlorsque vous entrez dans le menu ou tout ce qui devrait fonctionner avec la minuterie d'inactivité active et enableIdleTimerest appelée à partir de la applicationWillResignActiveméthode de votre AppDelegate pour garantir que toutes vos modifications sont correctement réinitialisées au comportement par défaut du système.
J'ai écrit un article et fourni le code de la classe singleton IdleTimerManager Idle Timer Handling in iPhone Games

Kay
la source
4

Voici une autre façon de détecter l'activité:

Le minuteur est ajouté UITrackingRunLoopMode, de sorte qu'il ne peut se déclencher qu'en cas d' UITrackingactivité. Il a également l'avantage de ne pas vous envoyer de spam pour tous les événements tactiles, vous informant ainsi s'il y a eu une activité dans les dernières ACTIVITY_DETECT_TIMER_RESOLUTIONsecondes. J'ai nommé le sélecteur keepAlivecar il semble être un cas d'utilisation approprié pour cela. Vous pouvez bien sûr faire ce que vous désirez avec l'information selon laquelle il y a eu une activité récemment.

_touchesTimer = [NSTimer timerWithTimeInterval:ACTIVITY_DETECT_TIMER_RESOLUTION
                                        target:self
                                      selector:@selector(keepAlive)
                                      userInfo:nil
                                       repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_touchesTimer forMode:UITrackingRunLoopMode];
Mihai Timar
la source
comment? Je crois qu'il est clair que vous devriez vous faire un sélecteur "keepAlive" vous-même pour tous vos besoins. Peut-être que je manque votre point de vue?
Mihai Timar le
Vous dites que c'est une autre façon de détecter l'activité, cependant, cela instancie uniquement un iVar qui est un NSTimer. Je ne vois pas comment cela répond à la question du PO.
Jasper
1
Le minuteur est ajouté dans UITrackingRunLoopMode, de sorte qu'il ne peut se déclencher que s'il y a une activité UITracking. Il a également l'avantage de ne pas vous envoyer de spam pour tous les événements tactiles, vous informant ainsi s'il y a eu une activité dans les dernières secondes de ACTIVITY_DETECT_TIMER_RESOLUTION. J'ai nommé le sélecteur keepAlive car il semble être un cas d'utilisation approprié pour cela. Vous pouvez bien sûr faire ce que vous désirez avec l'information selon laquelle il y a eu une activité récemment.
Mihai Timar
1
Je voudrais améliorer cette réponse. Si vous pouvez aider avec des conseils pour clarifier les choses, ce serait d'une grande aide.
Mihai Timar
J'ai ajouté votre explication à votre réponse. Cela a beaucoup plus de sens maintenant.
Jasper
3

En fin de compte, vous devez définir ce que vous considérez comme inactif: l'inactivité est-elle le résultat du fait que l'utilisateur ne touche pas l'écran ou est-ce l'état du système si aucune ressource informatique n'est utilisée? Il est possible, dans de nombreuses applications, que l'utilisateur fasse quelque chose même s'il n'interagit pas activement avec l'appareil via l'écran tactile. Bien que l'utilisateur soit probablement familier avec le concept de mise en veille de l'appareil et la notification que cela se produira via la gradation de l'écran, il n'est pas nécessairement le cas qu'il s'attend à ce que quelque chose se passe s'il est inactif - vous devez être prudent sur ce que vous feriez. Mais revenons à la déclaration originale - si vous considérez que le premier cas est votre définition, il n'y a pas de moyen vraiment facile de le faire. Vous auriez besoin de recevoir chaque événement tactile, le transmettre sur la chaîne de répondeurs au besoin tout en notant l'heure à laquelle il a été reçu. Cela vous donnera une base pour faire le calcul inactif. Si vous considérez que le deuxième cas est votre définition, vous pouvez jouer avec une notification NSPostWhenIdle pour essayer d'exécuter votre logique à ce moment-là.

sagequark
la source
1
Juste pour clarifier, je parle d'interaction avec l'écran. Je vais mettre à jour la question pour refléter cela.
Mike McMaster
1
Ensuite, vous pouvez implémenter quelque chose où chaque fois qu'un contact se produit, vous mettez à jour une valeur que vous vérifiez, ou même définissez (et réinitialisez) une minuterie d'inactivité pour qu'elle se déclenche, mais vous devez l'implémenter vous-même, car, comme l'a dit Wisequark, ce qui constitue l'inactivité varie entre différentes applications.
Louis Gerbarg
1
Je définis «inactif» strictement comme le temps écoulé depuis le dernier contact avec l'écran. Je comprends que je vais devoir l'implémenter moi-même, je me demandais simplement quelle serait la "meilleure" façon de, disons, intercepter les touches d'écran, ou si quelqu'un connaît une méthode alternative pour déterminer cela.
Mike McMaster
3

Il existe un moyen de faire cette application à l'échelle sans que les contrôleurs individuels aient à faire quoi que ce soit. Ajoutez simplement un outil de reconnaissance de gestes qui n'annule pas les touches. De cette façon, toutes les touches seront suivies pour le chronomètre, et les autres touches et gestes ne seront pas du tout affectés, donc personne d'autre n'a à le savoir.

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

Dans la méthode de lancement de la fin de votre délégué d'application, appelez simplement addGesture et vous êtes prêt. Toutes les touches passeront par les méthodes de CatchAllGesture sans que cela empêche la fonctionnalité des autres.

Jlam
la source
1
J'aime cette approche, je l'ai utilisée pour un problème similaire avec Xamarin: stackoverflow.com/a/51727021/250164
Wolfgang Schreurs
1
Fonctionne très bien, semble également que cette technique soit utilisée pour contrôler la visibilité des contrôles de l'interface utilisateur dans AVPlayerViewController ( référence API privée ). Le remplacement de l'application -sendEvent:est excessif et UITrackingRunLoopModene gère pas de nombreux cas.
Roman B.
@RomanB. Oui exactement. lorsque vous avez travaillé avec iOS assez longtemps, vous savez qu'il faut toujours utiliser "la bonne manière" et c'est la manière la plus simple d'implémenter un geste personnalisé comme prévu developer.apple.com/documentation/uikit/uigesturerecognizer
...
Qu'est-ce que la définition de l'état sur .failed accomplit dans touchesEnded?
stonedauwg
J'aime cette approche, mais lorsqu'elle est essayée, elle ne semble attraper que des robinets, pas un autre geste comme un panoramique, un balayage, etc. en touches. Était-ce l'intention?
stonedauwg