iPhone: detección de inactividad del usuario / tiempo de inactividad desde la última pantalla táctil

152

¿Alguien ha implementado una función en la que si el usuario no ha tocado la pantalla durante un cierto período de tiempo, usted toma una determinada acción? Estoy tratando de encontrar la mejor manera de hacerlo.

Existe este método algo relacionado en UIApplication:

[UIApplication sharedApplication].idleTimerDisabled;

Sería bueno si tuvieras algo como esto:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Entonces podría configurar un temporizador y verificar periódicamente este valor, y tomar alguna acción cuando exceda un umbral.

Espero que eso explique lo que estoy buscando. ¿Alguien ha abordado este problema o tiene alguna idea sobre cómo lo haría? Gracias.

Mike McMaster
fuente
Esta es una gran pregunta. Windows tiene el concepto de un evento OnIdle, pero creo que se trata más de que la aplicación actualmente no maneja nada en su bomba de mensajes frente a la propiedad idleTimerDisabled de iOS que solo parece preocupada por bloquear el dispositivo. ¿Alguien sabe si hay algo remotamente cercano al concepto de Windows en iOS / MacOSX?
stonedauwg

Respuestas:

153

Aquí está la respuesta que había estado buscando:

Haga que su aplicación delegue la subclase UIApplication. En el archivo de implementación, anule el método sendEvent: así:

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

donde maxIdleTime e idleTimer son variables de instancia.

Para que esto funcione, también debe modificar su main.m para decirle a UIApplicationMain que use su clase delegada (en este ejemplo, AppDelegate) como la clase principal:

int retVal = UIApplicationMain(argc, argv, @"AppDelegate", @"AppDelegate");
Mike McMaster
fuente
3
Hola Mike, My AppDelegate se está insertando de NSObject, así que lo cambié UIApplication e Implementé los métodos anteriores para detectar que el usuario está inactivo, pero recibo el error "Finalizando la aplicación debido a la excepción no detectada 'NSInternalInconsistencyException', razón:" Solo puede haber una instancia de UIApplication ". "... ¿Hay algo más que deba hacer ...?
Mihir Mehta
77
Agregaría que la subclase UIApplication debería estar separada de la subclase
UIApplicationDelegate
NO estoy seguro de cómo funcionará esto con el dispositivo entrando en estado inactivo cuando los temporizadores dejen de disparar.
anonmys
no funciona correctamente si asigno el uso de la función popToRootViewController para el evento de tiempo de espera. Sucede cuando muestro UIAlertView, luego popToRootViewController, luego presiono cualquier botón en UIAlertView con un selector de uiviewController que ya está abierto
Gargo
44
¡Muy agradable! Sin embargo, este enfoque crea muchas NSTimerinstancias si hay muchos toques.
Andreas Ley
86

Tengo una variación de la solución de temporizador inactivo que no requiere subclasificar la aplicación UIA. Funciona en una subclase específica de UIViewController, por lo que es útil si solo tiene un controlador de vista (como puede tener una aplicación o juego interactivo) o si solo desea manejar el tiempo de espera inactivo en un controlador de vista específico.

Tampoco vuelve a crear el objeto NSTimer cada vez que se reinicia el temporizador inactivo. Solo crea uno nuevo si se dispara el temporizador.

Su código puede llamar resetIdleTimera cualquier otro evento que pueda necesitar invalidar el temporizador inactivo (como una entrada significativa del acelerómetro).

@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

(código de limpieza de memoria excluido por brevedad).

Chris Miles
fuente
1
Muy bien. ¡Esta respuesta es genial! Es mejor que la respuesta marcada como correcta, aunque sé que fue mucho antes, pero esta es una mejor solución ahora.
Chintan Patel
Esto es genial, pero encontré un problema: desplazarse en UITableViews no hace que se llame a nextResponder. También probé el seguimiento a través de touchesBegan: y touchesMoved :, pero no hubo mejoras. ¿Algunas ideas?
Greg Maletic
3
@GregMaletic: tuve el mismo problema pero finalmente agregué: (nulo) scrollViewWillBeginDragging: (UIScrollView *) scrollView {NSLog (@ "Comenzará a arrastrar"); } - (vacío) scrollViewDidScroll: (UIScrollView *) scrollView {NSLog (@ "Did Scroll"); [self resetIdleTimer]; } ¿Has probado esto?
Akshay Aher 01 de
Gracias. Esto sigue siendo útil. Lo porté a Swift y funcionó muy bien.
Mark.ewd
Eres una estrella del rock. felicitaciones
Pras
21

Para swift v 3.1

no olvides comentar esta línea en 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)
   }
} 

cree el archivo main.swif y agregue esto (el nombre es importante)

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

Observación de notificaciones en cualquier otra clase

NotificationCenter.default.addObserver(self, selector: #selector(someFuncitonName), name: Notification.Name.TimeOutUserInteraction, object: nil)
Sergey Stadnik
fuente
2
No entiendo por qué necesitamos un cheque if idleTimer != nilen el sendEvent()método?
Guangyu Wang
¿Cómo podemos establecer el valor timeoutInSecondsde la respuesta del servicio web?
User_1191
12

Este hilo fue de gran ayuda, y lo envolví en una subclase de UIWindow que envía notificaciones. Elegí notificaciones para que sea un acoplamiento realmente flojo, pero puedes agregar un delegado con bastante facilidad.

Aquí está la esencia:

http://gist.github.com/365998

Además, la razón del problema de la subclase UIApplication es que la NIB está configurada para luego crear 2 objetos UIApplication ya que contiene la aplicación y el delegado. Sin embargo, la subclase UIWindow funciona muy bien.

Brian King
fuente
1
¿me puede decir cómo usar su código? No entiendo cómo
llamarlo
2
Funciona muy bien para toques, pero no parece manejar la entrada del teclado. Esto significa que se agotará el tiempo de espera si el usuario está escribiendo cosas en el teclado gui.
Martin Wickman
2
Yo tampoco puedo entender cómo usarlo ... agrego observadores en mi controlador de vista y espero que se activen las notificaciones cuando la aplicación está intacta / inactiva ... pero no pasó nada ... ¿y desde dónde podemos controlar el tiempo de inactividad? como si quisiera un tiempo de inactividad de 120 segundos para que después de 120 segundos se activara IdleNotification, no antes de eso.
Respuesta del
5

En realidad, la idea de subclases funciona muy bien. Simplemente no hagas que tu delegado sea la UIApplicationsubclase. Cree otro archivo que herede de UIApplication(por ejemplo, myApp). En IB, establezca la clase del fileOwnerobjeto en myAppy en myApp.m implemente el sendEventmétodo como se indica arriba. En main.m do:

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

et voilà!

Roby
fuente
1
Sí, crear una subclase de aplicación UIA independiente parece funcionar bien. Dejé el segundo parm nil en main.
Hot Licks
@Roby, mira que mi consulta stackoverflow.com/questions/20088521/… .
Tirth
4

Acabo de encontrarme con este problema con un juego que está controlado por movimientos, es decir, tiene el bloqueo de pantalla deshabilitado pero debería habilitarlo nuevamente cuando esté en el modo de menú. En lugar de un temporizador, encapsulé todas las llamadas setIdleTimerDisableddentro de una clase pequeña proporcionando los siguientes métodos:

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

disableIdleTimerdesactiva el temporizador inactivo, enableIdleTimerDelayedal ingresar al menú o lo que sea que se ejecute con el temporizador inactivo activo y enableIdleTimerse llama desde el applicationWillResignActivemétodo de AppDelegate para garantizar que todos los cambios se restablezcan correctamente al comportamiento predeterminado del sistema.
Escribí un artículo y proporcioné el código para la clase de singleton IdleTimerManager Idle Timer Handling en juegos de iPhone

Kay
fuente
4

Aquí hay otra forma de detectar actividad:

Se agrega el temporizador UITrackingRunLoopMode, por lo que solo puede disparar si hay UITrackingactividad. También tiene la ventaja de no enviarle correos no deseados para todos los eventos táctiles, informando así si hubo actividad en los últimos ACTIVITY_DETECT_TIMER_RESOLUTIONsegundos. Llamé al selector keepAliveya que parece un caso de uso apropiado para esto. Por supuesto, puede hacer lo que desee con la información de que hubo actividad recientemente.

_touchesTimer = [NSTimer timerWithTimeInterval:ACTIVITY_DETECT_TIMER_RESOLUTION
                                        target:self
                                      selector:@selector(keepAlive)
                                      userInfo:nil
                                       repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_touchesTimer forMode:UITrackingRunLoopMode];
Mihai Timar
fuente
¿Cómo es eso? creo que está claro que debes hacerte seleccionador "keepAlive" para cualquier necesidad que tengas. ¿Tal vez me estoy perdiendo tu punto de vista?
Mihai Timar
Usted dice que esta es otra forma de detectar actividad, sin embargo, esto solo crea una instancia de iVar que es un NSTimer. No veo cómo esto responde la pregunta del OP.
Jasper
1
El temporizador se agrega en UITrackingRunLoopMode, por lo que solo puede activarse si hay actividad de UITracking. También tiene la ventaja de no enviarle correos no deseados para todos los eventos táctiles, informando así si hubo actividad en los últimos ACTIVITY_DETECT_TIMER_RESOLUTION segundos. Llamé al selector keepAlive, ya que parece un caso de uso apropiado para esto. Por supuesto, puede hacer lo que desee con la información de que hubo actividad recientemente.
Mihai Timar
1
Me gustaría mejorar esta respuesta. Si puede ayudar con los consejos para aclararlo, sería de gran ayuda.
Mihai Timar
Agregué tu explicación a tu respuesta. Tiene mucho más sentido ahora.
Jasper
3

En última instancia, debe definir lo que considera inactivo: ¿está inactivo el resultado de que el usuario no toca la pantalla o es el estado del sistema si no se utilizan recursos informáticos? Es posible, en muchas aplicaciones, que el usuario haga algo incluso si no interactúa activamente con el dispositivo a través de la pantalla táctil. Si bien el usuario probablemente esté familiarizado con el concepto de que el dispositivo se va a dormir y el aviso de que sucederá a través de la atenuación de la pantalla, no es necesariamente el caso de que esperen que ocurra algo si están inactivos; debe tener cuidado sobre lo que harías Pero volviendo a la declaración original: si considera que el primer caso es su definición, no hay una manera realmente fácil de hacerlo. Debería recibir cada evento táctil, pasarlo a lo largo de la cadena de respuesta según sea necesario, al tiempo que señala la hora en que se recibió. Eso le dará alguna base para hacer el cálculo inactivo. Si considera que el segundo caso es su definición, puede jugar con una notificación NSPostWhenIdle para intentar realizar su lógica en ese momento.

wisequark
fuente
1
Solo para aclarar, estoy hablando de la interacción con la pantalla. Actualizaré la pregunta para reflejar eso.
Mike McMaster el
1
Luego, puede implementar algo donde cada vez que ocurre un toque, actualiza un valor que verifica, o incluso establece (y restablece) un temporizador inactivo para que se active, pero debe implementarlo usted mismo, porque como dijo wisequark, lo que constituye inactivo varía entre diferentes aplicaciones
Louis Gerbarg, el
1
Estoy definiendo "inactivo" estrictamente como el tiempo desde la última vez que toqué la pantalla. Entiendo que tendré que implementarlo yo mismo, me preguntaba cuál sería la "mejor" forma de, por ejemplo, interceptar toques de pantalla, o si alguien conoce un método alternativo para determinar esto.
Mike McMaster el
3

Hay una manera de hacer esta aplicación de manera amplia sin que los controladores individuales tengan que hacer nada. Simplemente agregue un reconocedor de gestos que no cancele toques. De esta manera, todos los toques se rastrearán para el temporizador, y otros toques y gestos no se verán afectados en absoluto, por lo que nadie más tiene que saberlo.

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

En el delegado de su aplicación, terminó el método de inicio, simplemente llame a addGesture y ya está todo listo. Todos los toques pasarán por los métodos de CatchAllGesture sin que esto impida la funcionalidad de otros.

Jlam
fuente
1
Me gusta este enfoque, lo usé para un problema similar con Xamarin: stackoverflow.com/a/51727021/250164
Wolfgang Schreurs
1
Funciona muy bien, también parece que esta técnica se utiliza para controlar la visibilidad de los controles de la interfaz de usuario en AVPlayerViewController ( referencia de API privada ). La anulación de la aplicación -sendEvent:es exagerada y UITrackingRunLoopModeno maneja muchos casos.
Roman B.
@RomanB. si, exacto. cuando ha trabajado con iOS el tiempo suficiente, sabe que siempre debe usar "la forma correcta" y esta es la forma más sencilla de implementar un gesto personalizado como desarrollador.apple.com/documentation/uikit/uigesturerecognizer/…
Jlam
¿Qué logra establecer el estado en .failed en touchesEnded?
stonedauwg
Me gusta este enfoque, pero cuando lo intento, solo parece captar toques, no cualquier otro gesto como paneo, deslizar, etc. en toques En dónde se iría la lógica de reinicio. ¿Fue eso intencionado?
stonedauwg