iphone - problemas - ios 12
iPhone: detección de inactividad del usuario/tiempo de inactividad desde la última pantalla táctil (9)
Acabo de toparme con este problema con un juego que está controlado por movimientos, es decir, tiene el bloqueo de pantalla desactivado, pero debería habilitarlo de nuevo cuando esté en el modo de menú. En lugar de un temporizador, encapsulé todas las llamadas a setIdleTimerDisabled
dentro de una clase pequeña que proporciona 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];
}
disableIdleTimer
desactiva el temporizador inactivo, enableIdleTimerDelayed
al ingresar al menú o lo que deba ejecutarse con el temporizador inactivo activo y enableIdleTimer
se llama desde el método applicationWillResignActive
de AppDelegate para garantizar que todos sus cambios se restablecen correctamente al comportamiento predeterminado del sistema.
Escribí un artículo y proporcioné el código para la clase Singleton IdleTimerManager Idle Timer Handling en juegos de iPhone
¿Alguien ha implementado una función donde si el usuario no ha tocado la pantalla durante un cierto período de tiempo, realiza una determinada acción? Estoy tratando de encontrar la mejor manera de hacerlo.
Hay un método algo relacionado en UIApplication:
[UIApplication sharedApplication].idleTimerDisabled;
Sería bueno si en cambio tuvieras algo como esto:
NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;
Entonces podría configurar un temporizador y verificar periódicamente este valor, y tomar alguna medida cuando exceda un umbral.
Espero que eso explique lo que estoy buscando. ¿Alguien ya ha abordado este tema o tiene alguna idea de cómo lo haría? Gracias.
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 y idleTimer son variables de instancia.
Para que esto funcione, también necesita modificar su main.m para decirle a UIApplicationMain que use su clase de delegado (en este ejemplo, AppDelegate) como clase principal:
int retVal = UIApplicationMain(argc, argv, @"AppDelegate", @"AppDelegate");
Aquí hay otra forma de detectar actividad:
El temporizador se agrega en UITrackingRunLoopMode
, por lo que solo puede UITracking
si hay actividad de UITracking
. También tiene la buena ventaja de no enviarle correos no deseados por todos los eventos táctiles, lo que le informa 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.
_touchesTimer = [NSTimer timerWithTimeInterval:ACTIVITY_DETECT_TIMER_RESOLUTION
target:self
selector:@selector(keepAlive)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_touchesTimer forMode:UITrackingRunLoopMode];
En última instancia, debe definir lo que considera que está inactivo: ¿está inactivo el resultado de que el usuario no toque 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 aunque no interactúe 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 del oscurecimiento 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 afirmación original: si considera que el primer caso es su definición, no hay una manera realmente fácil de hacerlo. Necesitaría recibir cada evento táctil, pasarlo a lo largo de la cadena de respuesta según sea necesario, al tiempo de anotar 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 probar y ejecutar su lógica en ese momento.
En realidad, la idea de subclases funciona muy bien. Simplemente no haga que delegue la subclase UIApplication
. Cree otro archivo que herede de UIApplication
(por ejemplo, myApp). En IB, establezca la clase del objeto fileOwner
en myApp
y en myApp.m implemente el método sendEvent
como se sendEvent
anteriormente. En main.m do:
int retVal = UIApplicationMain(argc,argv,@"myApp.m",@"myApp.m")
et voilà!
Este hilo fue de gran ayuda, y lo envolví en una subclase UIWindow que envía notificaciones. Elegí las notificaciones para que sea un acoplamiento realmente flexible, pero puede agregar un delegado con la suficiente facilidad.
Aquí está la esencia:
Además, la razón del problema de la subclase UIApplication es que el NIB está configurado para crear 2 objetos UIApplication ya que contiene la aplicación y el delegado. La subclase UIWindow funciona muy bien.
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)
}
}
crea el archivo main.swif y agrega 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))
}
Observando la notificación en cualquier otra clase
NotificationCenter.default.addObserver(self, selector: #selector(someFuncitonName), name: Notification.Name.TimeOutUserInteraction, object: nil)
Tengo una variación de la solución del temporizador inactivo que no requiere subclases UIApplication. Funciona en una subclase específica UIViewController, por lo que es útil si solo tiene un controlador de vista (como una aplicación interactiva o un juego puede tener) o 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 restablece el temporizador de inactividad. Solo crea uno nuevo si el temporizador se dispara.
Su código puede llamar a resetIdleTimer
para cualquier otro evento que pueda necesitar invalidar el temporizador inactivo (como la entrada de un acelerómetro significativo).
@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).
Todo esto parece demasiado complejo. Creo que esta es una forma más limpia de usar todos los toques que necesita.
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 su aplicación, el delegado finalizó el método de lanzamiento, simplemente llame a addGesture y ya está todo listo. Todos los toques se realizarán mediante los métodos de CatchAllGesture sin que esto impida la funcionalidad de los demás.