iPhone: rilevamento dell'inattività dell'utente / tempo di inattività dall'ultimo tocco dello schermo


152

Qualcuno ha implementato una funzione in cui se l'utente non ha toccato lo schermo per un certo periodo di tempo, si intraprende una determinata azione? Sto cercando di capire il modo migliore per farlo.

C'è questo metodo in qualche modo correlato in UIApplication:

[UIApplication sharedApplication].idleTimerDisabled;

Sarebbe bello se invece avessi qualcosa del genere:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Quindi potrei impostare un timer e controllare periodicamente questo valore, e prendere qualche misura quando supera una soglia.

Spero che questo spieghi cosa sto cercando. Qualcuno ha già affrontato questo problema o hai qualche idea su come lo faresti? Grazie.


Questa è un'ottima domanda Windows ha il concetto di un evento OnIdle ma penso che si tratti più dell'app che attualmente non gestisce nulla nel suo pump dei messaggi rispetto alla proprietà iOS idleTimerDisabled che sembra interessata solo al blocco del dispositivo. Qualcuno sa se c'è qualcosa di lontanamente vicino al concetto di Windows in iOS / MacOSX?
stonedauwg,

Risposte:


153

Ecco la risposta che stavo cercando:

Chiedi alla tua sottoclasse dell'applicazione di delegare UIApplication. Nel file di implementazione, sovrascrivere il metodo sendEvent: in questo modo:

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

dove maxIdleTime e idleTimer sono variabili di istanza.

Affinché ciò funzioni, devi anche modificare il tuo main.m per dire a UIApplicationMain di usare la tua classe delegata (in questo esempio, AppDelegate) come classe principale:

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

3
Ciao Mike, My AppDelegate eredita da NSObject Quindi ha cambiato UIApplication e Implementa i metodi precedenti per rilevare l'utente diventare inattivo ma sto ricevendo un errore "Terminare l'app a causa di un'eccezione non rilevata" NSInternalInconsistencyException ", motivo:" Può esserci solo un'istanza UIApplication ". ".. è qualcos'altro che devo fare ...?
Mihir Mehta,

7
Aggiungo che la sottoclasse UIApplication dovrebbe essere separata dalla sottoclasse
UIApplicationDelegate

NON sono sicuro di come funzionerà con il dispositivo che entra nello stato inattivo quando i timer smettono di sparare?
anonmys,

non funziona correttamente se assegnerò l'utilizzo della funzione popToRootViewController per l'evento di timeout. Succede quando mostro UIAlertView, poi popToRootViewController, quindi premo qualsiasi pulsante su UIAlertView con un selettore di uiviewController che è già stato visualizzato
Gargo

4
Molto bella! Tuttavia, questo approccio crea molti NSTimercasi se ci sono molti tocchi.
Andreas Ley,

86

Ho una variante della soluzione del timer di inattività che non richiede la sottoclasse di UIApplication. Funziona su una sottoclasse UIViewController specifica, quindi è utile se hai un solo controller di visualizzazione (come un'app interattiva o un gioco potrebbe avere) o desideri gestire il timeout di inattività in un controller di visualizzazione specifico.

Inoltre, non ricrea l'oggetto NSTimer ogni volta che il timer di inattività viene ripristinato. Ne crea uno nuovo solo se il timer scatta.

Il tuo codice può chiamare resetIdleTimerqualsiasi altro evento che potrebbe essere necessario invalidare il timer di inattività (come un significativo input dell'accelerometro).

@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

(codice di pulizia della memoria escluso per brevità.)


1
Molto bene. Questa risposta è incredibile! Batte la risposta contrassegnata come corretta anche se so che era molto prima, ma questa è una soluzione migliore ora.
Chintan Patel,

Questo è fantastico, ma ho riscontrato un problema: lo scorrimento in UITableViews non causa la chiamata a nextResponder. Ho anche provato a tracciare tramite touchesBegan: e touchMoved :, ma nessun miglioramento. Qualche idea?
Greg Maletic,

3
@GregMaletic: ho avuto lo stesso problema ma alla fine ho aggiunto - (vuoto) scrollViewWillBeginDragging: (UIScrollView *) scrollView {NSLog (@ "Inizierà a trascinare"); } - (void) scrollViewDidScroll: (UIScrollView *) scrollView {NSLog (@ "Did Scroll"); [auto resetIdleTimer]; } hai provato questo?
Akshay Aher,

Grazie. Questo è ancora utile. L'ho portato su Swift e ha funzionato alla grande.
Mark.ewd,

Sei una rock star. complimenti
Pras,

21

Per swift v 3.1

non dimenticare di commentare questa riga in 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)
   }
} 

crea il file main.swif e aggiungi questo (il nome è importante)

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

Osservando la notifica in qualsiasi altra classe

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

2
Non capisco perché abbiamo bisogno di un metodo di check- if idleTimer != nilin sendEvent()?
Guangyu Wang,

Come possiamo impostare un valore timeoutInSecondsdalla risposta del servizio Web?
User_1191

12

Questo thread è stato di grande aiuto e l'ho inserito in una sottoclasse UIWindow che invia notifiche. Ho scelto le notifiche per renderlo un vero accoppiamento libero, ma puoi aggiungere un delegato abbastanza facilmente.

Ecco l'essenza:

http://gist.github.com/365998

Inoltre, il motivo del problema della sottoclasse UIApplication è che il NIB è configurato per creare quindi 2 oggetti UIApplication poiché contiene l'applicazione e il delegato. La sottoclasse UIWindow funziona benissimo.


1
puoi dirmi come usare il tuo codice? non capisco come chiamarlo
R. Dewi

2
Funziona benissimo per i tocchi, ma non sembra gestire l'input dalla tastiera. Ciò significa che scadrà se l'utente sta digitando elementi sulla tastiera grafica.
Martin Wickman,

2
Anch'io non sono in grado di capire come usarlo ... Aggiungo osservatori nel mio controller di visualizzazione e mi aspetto che le notifiche vengano attivate quando l'app non è toccata / inattiva ... ma non è successo nulla ... oltre a dove possiamo controllare i tempi di inattività? come se volessi un tempo di inattività di 120 secondi in modo che dopo 120 secondi la notifica di inattività dovesse attivarsi, non prima.
Ans il

5

In realtà l'idea di sottoclasse funziona alla grande. Basta non rendere il tuo delegato la UIApplicationsottoclasse. Crea un altro file che eredita da UIApplication(ad esempio myApp). In IB impostare la classe delfileOwner dell'oggetto su myAppe in myApp.m implementare il sendEventmetodo come sopra. In main.m fai:

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

et voilà!


1
Sì, la creazione di una sottoclasse UIApplication autonoma sembra funzionare correttamente. Ho lasciato il secondo paragrafo in principale.
Hot Licks

@Roby, dai un'occhiata alla mia query stackoverflow.com/questions/20088521/… .
Tirth,

4

Ho appena riscontrato questo problema con un gioco controllato da movimenti, ad esempio il blocco dello schermo è disabilitato ma dovrebbe attivarlo di nuovo in modalità menu. Invece di un timer ho incapsulato tutte le chiamate setIdleTimerDisabledall'interno di una piccola classe fornendo i seguenti metodi:

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

disableIdleTimerdisattiva il timer di inattività, enableIdleTimerDelayedquando si accede al menu o qualsiasi cosa debba essere eseguito con il timer di inattività attivo e enableIdleTimerviene chiamato dal applicationWillResignActivemetodo di AppDelegate per garantire che tutte le modifiche vengano ripristinate correttamente al comportamento predefinito del sistema.
Ho scritto un articolo e fornito il codice per la classe singleton IdleTimerManager Idle Timer Handling nei giochi per iPhone


4

Ecco un altro modo per rilevare l'attività:

Il timer viene aggiunto UITrackingRunLoopMode, quindi può attivarsi solo se c'è UITrackingattività. Ha anche il piacevole vantaggio di non inviarti spam per tutti gli eventi touch, informando così se c'è stata attività negli ultimi ACTIVITY_DETECT_TIMER_RESOLUTIONsecondi. Ho chiamato il selettore keepAlivein quanto sembra un caso d'uso appropriato per questo. Ovviamente puoi fare quello che desideri con le informazioni che ci sono state attività di recente.

_touchesTimer = [NSTimer timerWithTimeInterval:ACTIVITY_DETECT_TIMER_RESOLUTION
                                        target:self
                                      selector:@selector(keepAlive)
                                      userInfo:nil
                                       repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_touchesTimer forMode:UITrackingRunLoopMode];

come mai? credo sia chiaro che dovresti farti selettore "keepAlive" per qualsiasi necessità tu abbia. Forse mi stai perdendo il tuo punto di vista?
Mihai Timar,

Dici che questo è un altro modo per rilevare l'attività, tuttavia, crea un'istanza solo un iVar che è un NSTimer. Non vedo come questo risponda alla domanda del PO.
Jasper,

1
Il timer viene aggiunto in UITrackingRunLoopMode, quindi può attivarsi solo se c'è attività UITracking. Ha anche il piacevole vantaggio di non inviarti spam per tutti gli eventi touch, informando così se c'è stata attività negli ultimi ACTIVITY_DETECT_TIMER_RESOLUTION secondi. Ho chiamato il selettore keepAlive in quanto sembra un caso d'uso appropriato per questo. Ovviamente puoi fare quello che desideri con le informazioni che ci sono state attività di recente.
Mihai Timar,

1
Vorrei migliorare questa risposta. Se puoi aiutare con i puntatori a renderlo più chiaro, sarebbe di grande aiuto.
Mihai Timar,

Ho aggiunto la tua spiegazione alla tua risposta. Adesso ha molto più senso.
Jasper,

3

In definitiva, è necessario definire ciò che si considera inattivo - è inattivo il risultato dell'utente che non tocca lo schermo o è lo stato del sistema se non vengono utilizzate risorse di elaborazione? È possibile, in molte applicazioni, che l'utente faccia qualcosa anche se non interagisce attivamente con il dispositivo tramite il touchscreen. Sebbene l'utente abbia probabilmente familiarità con il concetto del dispositivo che va in modalità sospensione e con la notifica che accadrà tramite oscuramento dello schermo, non è necessariamente il caso che si aspettino che accada qualcosa se sono inattivi - devi stare attento su cosa faresti. Ma tornando alla dichiarazione originale - se consideri il primo caso come la tua definizione, non esiste un modo davvero semplice per farlo. Dovresti ricevere ogni evento touch, passandolo lungo sulla catena del risponditore secondo necessità, osservando l'ora in cui è stato ricevuto. Questo ti darà alcune basi per fare il calcolo inattivo. Se consideri il secondo caso come la tua definizione, puoi giocare con una notifica NSPostWhenIdle per provare a eseguire la logica in quel momento.


1
Giusto per chiarire, sto parlando di interazione con lo schermo. Aggiornerò la domanda per riflettere ciò.
Mike McMaster,

1
Quindi puoi implementare qualcosa in cui ogni volta che si verifica un tocco aggiorni un valore che controlli, o addirittura imposti (e ripristini) un timer di inattività per attivarlo, ma devi implementarlo tu stesso, perché come diceva Wisequark, ciò che costituisce inattivo varia tra diverse app.
Louis Gerbarg,

1
Sto definendo "inattivo" rigorosamente come il tempo trascorso dall'ultimo tocco sullo schermo. Capisco che dovrò implementarlo da solo, mi stavo solo chiedendo quale sarebbe il modo "migliore" di, per esempio, intercettare i tocchi dello schermo, o se qualcuno conosce un metodo alternativo per determinarlo.
Mike McMaster,

3

C'è un modo per ampliare questa app senza che i singoli controller debbano fare nulla. Basta aggiungere un riconoscimento dei gesti che non annulla i tocchi. In questo modo, tutti i tocchi verranno tracciati per il timer e altri tocchi e gesti non saranno influenzati affatto, quindi nessun altro deve saperlo.

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

Nel modo in cui il delegato dell'app ha terminato il metodo di avvio, basta chiamare addGesture e il gioco è fatto. Tutti i tocchi passeranno attraverso i metodi di CatchAllGesture senza che ciò impedisca la funzionalità di altri.


1
Mi piace questo approccio, l'ho usato per un problema simile con Xamarin: stackoverflow.com/a/51727021/250164
Wolfgang Schreurs

1
Funziona alla grande, sembra anche che questa tecnica venga utilizzata per controllare la visibilità dei controlli dell'interfaccia utente in AVPlayerViewController (riferimento API privato ). L'override dell'app -sendEvent:è eccessiva e UITrackingRunLoopModenon gestisce molti casi.
Roman B.

@RomanB. si Esattamente. quando hai lavorato con iOS abbastanza a lungo sai di usare sempre "la strada giusta" e questo è il modo semplice per implementare un gesto personalizzato come previsto developer.apple.com/documentation/uikit/uigesturerecognizer/…
Jlam

Cosa consente di impostare lo stato su .failed in touchesEnded?
stonedauwg,

Mi piace questo approccio, ma quando viene provato sembra catturare solo tocchi, non qualsiasi altro gesto come pan, swipe ecc. In tocchi. Trova dove andrebbe la logica di reset. Era previsto?
stonedauwg
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.