web-dev-qa-db-fra.com

iPhone: détection de l'inactivité de l'utilisateur / du temps d'inactivité depuis le dernier contact avec l'écran

Quelqu'un a-t-il mis en œuvre une fonctionnalité dans laquelle, si l'utilisateur n'a pas touché l'écran pendant un certain temps, vous prenez une certaine mesure? J'essaie de trouver le meilleur moyen de le faire.

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

[UIApplication sharedApplication].idleTimerDisabled;

Ce serait bien si vous aviez plutôt quelque chose comme ça:

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 une idée de la façon dont vous le feriez? Merci.

150
Mike McMaster

Voici la réponse que je cherchais:

Demandez à votre application de déléguer la sous-classe UIApplication. Dans le fichier d'implémentation, substituez 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 delegate (dans cet exemple, AppDelegate) en tant que classe principale:

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

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

En outre, il ne recrée pas l'objet NSTimer chaque fois que le minuteur inactif est réinitialisé. Il en crée un nouveau uniquement si la minuterie se déclenche.

Votre code peut appeler resetIdleTimer pour tout autre événement susceptible d'invalider le minuteur inactif (tel qu'une entrée d'accéléromètre significative).

@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 pour des raisons de brièveté.)

86
Chris Miles

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éer un fichier main.swif et l'ajouter (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 une notification dans une autre classe

NotificationCenter.default.addObserver(self, selector: #selector(someFuncitonName), name: Notification.Name.TimeOutUserInteraction, object: nil)
18
Sergey Stadnik

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

Voici le Gist:

http://Gist.github.com/365998

De plus, le problème de la sous-classe UIApplication tient au fait que la carte NIB est configurée pour créer ensuite 2 objets UIApplication, car elle contient l'application et le délégué. La sous-classe UIWindow fonctionne très bien.

12
Brian King

Voici un autre moyen de détecter une activité:

La minuterie est ajoutée dans UITrackingRunLoopMode, elle ne peut donc être déclenchée que s'il y a une activité UITracking. Il a également l’avantage de ne pas vous spammer pour tous les événements tactiles, ce qui permet de savoir s’il y a eu une activité au cours de la dernière ACTIVITY_DETECT_TIMER_RESOLUTION secondes. J'ai nommé le sélecteur keepAlive car il semble que ce soit un cas d'utilisation approprié. Vous pouvez bien sûr faire ce que vous désirez avec l'information qu'il y a eu de l'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];
4
Mihai Timar

En fait, l'idée de sous-classes fonctionne très bien. Juste, ne faites pas de votre délégué la sous-classe UIApplication. Créez un autre fichier qui hérite de UIApplication (par exemple, monApp). Dans IB, définissez la classe de l'objet fileOwner sur myApp et dans myApp.m, implémentez la méthode sendEvent comme ci-dessus. En main.m faire:

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

et voilà!

4
Roby

Je viens de rencontrer ce problème avec un jeu contrôlé par des mouvements, c’est-à-dire que le verrouillage de l’écran est désactivé mais qu’il doit être réactivé en mode menu. Au lieu d'une minuterie, j'ai encapsulé tous les appels à setIdleTimerDisabled dans une petite classe en 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];
}

disableIdleTimer désactive le minuteur inactif, enableIdleTimerDelayed lorsque vous entrez dans le menu ou quoi que ce soit qui doit fonctionner avec un minuteur inactif actif et enableIdleTimer est appelé à partir de la méthode _ de votre AppDelegate applicationWillResignActive vos modifications sont réinitialisées correctement au comportement par défaut du système.
J'ai écrit un article et fourni le code de la classe singleton IdleTimerManager Gestion de la minuterie d'inactivité dans les jeux iPhone

4
Kay

Il existe un moyen de faire cette application large sans que les contrôleurs individuels aient à faire quoi que ce soit. Ajoutez simplement un identificateur de geste 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 affectés du tout, de sorte que 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 du délégué de votre application, appelez simplement addGesture et vous êtes prêt. Toutes les touches vont passer par les méthodes de CatchAllGesture sans que cela empêche la fonctionnalité des autres.

3
Jlam

En fin de compte, vous devez définir ce que vous considérez comme étant inactif: l’inactivité est-elle due au fait que l’utilisateur ne touche pas l’écran ou s’agit-il de l’état du système si aucune ressource informatique n’est utilisée? Dans de nombreuses applications, il est possible 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 connaisse probablement le principe de mise en veille de l'appareil et la notification que cela se produira via la gradation de l'écran, il n'est pas forcément certain qu'ils s'attendent à ce qu'il se passe quelque chose s'ils restent inactifs - vous devez faire attention. à propos de ce que vous feriez. Mais revenons à la déclaration initiale - si vous considérez le premier cas comme votre définition, il n’ya pas de moyen très simple de le faire. Vous devez recevoir chaque événement tactile en le transmettant au besoin à la chaîne de répondeurs tout en notant l'heure à laquelle il a été reçu. Cela vous donnera une base pour effectuer 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à.

3
wisequark