iPhone: Erkennen der Inaktivität / Leerlaufzeit des Benutzers seit der letzten Bildschirmberührung

152

Hat jemand eine Funktion implementiert, bei der Sie eine bestimmte Aktion ausführen, wenn der Benutzer den Bildschirm für einen bestimmten Zeitraum nicht berührt hat? Ich versuche herauszufinden, wie ich das am besten machen kann.

In UIApplication gibt es diese etwas verwandte Methode:

[UIApplication sharedApplication].idleTimerDisabled;

Es wäre schön, wenn Sie stattdessen so etwas hätten:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Dann könnte ich einen Timer einrichten und diesen Wert regelmäßig überprüfen und Maßnahmen ergreifen, wenn er einen Schwellenwert überschreitet.

Hoffentlich erklärt das, wonach ich suche. Hat sich bereits jemand mit diesem Problem befasst oder haben Sie sich Gedanken darüber gemacht, wie Sie es tun würden? Vielen Dank.

Mike McMaster
quelle
Das ist eine gute Frage. Windows hat das Konzept eines OnIdle-Ereignisses, aber ich denke, es geht eher darum, dass die App derzeit nichts in ihrer Nachrichtenpumpe verarbeitet, als um die iOS-Eigenschaft idleTimerDisabled, die sich anscheinend nur mit dem Sperren des Geräts befasst. Weiß jemand, ob es irgendetwas gibt, das dem Windows-Konzept in iOS / MacOSX auch nur annähernd nahe kommt?
Stonedauwg

Antworten:

153

Hier ist die Antwort, nach der ich gesucht hatte:

Lassen Sie Ihre Anwendung die Unterklasse UIApplication delegieren. Überschreiben Sie in der Implementierungsdatei die sendEvent: -Methode wie folgt:

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

Dabei sind maxIdleTime und idleTimer Instanzvariablen.

Damit dies funktioniert, müssen Sie auch Ihre main.m ändern, um UIApplicationMain anzuweisen, Ihre Delegatenklasse (in diesem Beispiel AppDelegate) als Hauptklasse zu verwenden:

int retVal = UIApplicationMain(argc, argv, @"AppDelegate", @"AppDelegate");
Mike McMaster
quelle
3
Hallo Mike, mein AppDelegate erbt von NSObject. Daher wurde die UIApplication geändert und die oben genannten Methoden implementiert, um zu erkennen, dass der Benutzer inaktiv ist. Es wird jedoch die Fehlermeldung "Beenden der App aufgrund einer nicht erfassten Ausnahme 'NSInternalInconsistencyException', Grund: 'Es kann nur eine UIApplication-Instanz geben.' ".. muss ich noch etwas tun ...?
Mihir Mehta
7
Ich würde hinzufügen, dass die UIApplication-Unterklasse von der UIApplicationDelegate-Unterklasse getrennt sein sollte
Boliva
Ich bin nicht sicher, wie dies funktioniert, wenn das Gerät in den inaktiven Zustand wechselt, wenn die Timer nicht mehr ausgelöst werden.
Anonmys
funktioniert nicht richtig, wenn ich die Funktion popToRootViewController für ein Timeout-Ereignis verwende. Es passiert, wenn ich UIAlertView zeige, dann popToRootViewController, dann drücke ich eine beliebige Taste auf UIAlertView mit einem Selektor von uiviewController, der bereits geöffnet ist
Gargo
4
Sehr schön! Dieser Ansatz erzeugt jedoch viele NSTimerInstanzen, wenn es viele Berührungen gibt.
Andreas Ley
86

Ich habe eine Variante der Idle-Timer-Lösung, für die keine UIA-Anwendung der Unterklasse erforderlich ist. Es funktioniert in einer bestimmten UIViewController-Unterklasse und ist daher nützlich, wenn Sie nur einen View-Controller haben (wie eine interaktive App oder ein Spiel) oder nur das Leerlaufzeitlimit in einem bestimmten View-Controller verarbeiten möchten.

Außerdem wird das NSTimer-Objekt nicht jedes Mal neu erstellt, wenn der Leerlauf-Timer zurückgesetzt wird. Es wird nur dann ein neuer erstellt, wenn der Timer ausgelöst wird.

Ihr Code kann resetIdleTimeralle anderen Ereignisse aufrufen , die möglicherweise den Leerlauf-Timer ungültig machen müssen (z. B. die Eingabe eines signifikanten Beschleunigungsmessers).

@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

(Speicherbereinigungscode aus Gründen der Kürze ausgeschlossen.)

Chris Miles
quelle
1
Sehr gut. Diese Antwort rockt! Schlägt die als richtig markierte Antwort, obwohl ich weiß, dass es viel früher war, aber dies ist jetzt eine bessere Lösung.
Chintan Patel
Das ist großartig, aber ein Problem, das ich gefunden habe: Durch das Scrollen in UITableViews wird nextResponder nicht aufgerufen. Ich habe auch versucht, über touchBegan: und touchMoved: zu verfolgen, aber keine Verbesserung. Irgendwelche Ideen?
Greg Maletic
3
@GregMaletic: Ich hatte das gleiche Problem, aber schließlich habe ich hinzugefügt - (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView {NSLog (@ "Wird anfangen zu ziehen"); } - (void) scrollViewDidScroll: (UIScrollView *) scrollView {NSLog (@ "Did Scroll"); [self resetIdleTimer]; } Hast du das versucht?
Akshay Aher
Vielen Dank. Das ist immer noch hilfreich. Ich habe es auf Swift portiert und es hat super funktioniert.
Mark.ewd
Du bist ein Rockstar. Kudos
Pras
21

Für schnelle v 3.1

Vergessen Sie nicht, diese Zeile in AppDelegate // @ UIApplicationMain zu kommentieren

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

Erstellen Sie die Datei main.swif und fügen Sie diese hinzu (Name ist wichtig)

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

Benachrichtigung in einer anderen Klasse beobachten

NotificationCenter.default.addObserver(self, selector: #selector(someFuncitonName), name: Notification.Name.TimeOutUserInteraction, object: nil)
Sergey Stadnik
quelle
2
Ich verstehe nicht, warum wir eine Check- if idleTimer != nilin- sendEvent()Methode benötigen ?
Guangyu Wang
Wie können wir den Wert für die timeoutInSecondsAntwort des Webdienstes festlegen ?
User_1191
12

Dieser Thread war eine große Hilfe, und ich habe ihn in eine UIWindow-Unterklasse eingepackt, die Benachrichtigungen sendet. Ich habe Benachrichtigungen ausgewählt, um eine echte lose Kopplung zu erzielen, aber Sie können problemlos einen Delegaten hinzufügen.

Hier ist das Wesentliche:

http://gist.github.com/365998

Der Grund für das Problem mit der UIApplication-Unterklasse besteht darin, dass die NIB so eingerichtet ist, dass dann zwei UIApplication-Objekte erstellt werden, da sie die Anwendung und den Delegaten enthält. Die UIWindow-Unterklasse funktioniert jedoch hervorragend.

Brian King
quelle
1
Können Sie mir sagen, wie Sie Ihren Code verwenden sollen? Ich verstehe nicht, wie ich es nennen soll
R. Dewi
2
Es eignet sich hervorragend für Berührungen, scheint jedoch keine Eingaben über die Tastatur zu verarbeiten. Dies bedeutet, dass eine Zeitüberschreitung auftritt, wenn der Benutzer Inhalte auf der GUI-Tastatur eingibt.
Martin Wickman
2
Auch ich kann nicht verstehen, wie man es benutzt ... Ich füge Beobachter in meinen Ansichts-Controller ein und erwarte, dass Benachrichtigungen ausgelöst werden, wenn die App unberührt / inaktiv ist. Aber nichts ist passiert. Und von wo aus können wir die Leerlaufzeit steuern? Ich möchte eine Leerlaufzeit von 120 Sekunden, damit IdleNotification nach 120 Sekunden ausgelöst wird, nicht vorher.
Ans
5

Eigentlich funktioniert die Idee der Unterklasse großartig. Machen Sie Ihren Delegierten nur nicht zur UIApplicationUnterklasse. Erstellen Sie eine andere Datei, die von UIApplication(z. B. myApp) erbt . Setzen Sie in IB die Klasse des fileOwnerObjekts auf myAppund implementieren Sie in myApp.m die sendEventMethode wie oben. In main.m tun:

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

et voilà!

Roby
quelle
1
Ja, das Erstellen einer eigenständigen UIApplication-Unterklasse scheint gut zu funktionieren. Ich habe den zweiten Parm Null in der Hauptsache gelassen.
Hot Licks
@Roby, schauen Sie, dass meine Abfrage stackoverflow.com/questions/20088521/… .
Tirth
4

Ich bin gerade auf dieses Problem mit einem Spiel gestoßen, das durch Bewegungen gesteuert wird, dh die Bildschirmsperre ist deaktiviert, sollte es aber im Menümodus wieder aktivieren. Anstelle eines Timers habe ich alle Aufrufe setIdleTimerDisabledinnerhalb einer kleinen Klasse mit den folgenden Methoden gekapselt :

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

disableIdleTimerDeaktiviert den Leerlauf-Timer, enableIdleTimerDelayedwenn Sie das Menü enableIdleTimeraufrufen oder was auch immer mit aktivem Leerlauf-Timer ausgeführt werden soll, und wird von der AppDelegate- applicationWillResignActiveMethode aufgerufen, um sicherzustellen, dass alle Ihre Änderungen ordnungsgemäß auf das Standardverhalten des Systems zurückgesetzt werden.
Ich habe einen Artikel geschrieben und den Code für die Singleton-Klasse IdleTimerManager Idle Timer Handling in iPhone-Spielen bereitgestellt

Kay
quelle
4

Hier ist eine andere Möglichkeit, Aktivität zu erkennen:

Der Timer wird hinzugefügt UITrackingRunLoopMode, sodass er nur bei UITrackingAktivität ausgelöst werden kann. Es hat auch den schönen Vorteil, dass Sie nicht für alle Berührungsereignisse gespammt werden, um zu informieren, ob in den letzten ACTIVITY_DETECT_TIMER_RESOLUTIONSekunden Aktivität stattgefunden hat. Ich habe den Selektor benannt, keepAliveda dies ein angemessener Anwendungsfall ist. Sie können natürlich mit den Informationen, dass in letzter Zeit Aktivitäten stattgefunden haben, tun, was Sie wollen.

_touchesTimer = [NSTimer timerWithTimeInterval:ACTIVITY_DETECT_TIMER_RESOLUTION
                                        target:self
                                      selector:@selector(keepAlive)
                                      userInfo:nil
                                       repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_touchesTimer forMode:UITrackingRunLoopMode];
Mihai Timar
quelle
Wie das? Ich glaube, es ist klar, dass Sie sich selbst zum "KeepAlive" -Selektor machen sollten, um alle Ihre Bedürfnisse zu erfüllen. Vielleicht vermisse ich deinen Standpunkt?
Mihai Timar
Sie sagen, dass dies eine andere Möglichkeit ist, Aktivitäten zu erkennen. Dies instanziiert jedoch nur einen iVar, der ein NSTimer ist. Ich sehe nicht, wie dies die Frage des OP beantwortet.
Jasper
1
Der Timer wird in UITrackingRunLoopMode hinzugefügt, sodass er nur ausgelöst werden kann, wenn UITracking-Aktivitäten vorhanden sind. Es hat auch den schönen Vorteil, dass Sie nicht für alle Berührungsereignisse gespammt werden und somit informiert werden, ob in den letzten Sekunden von ACTIVITY_DETECT_TIMER_RESOLUTION Aktivität aufgetreten ist. Ich habe den Selektor keepAlive genannt, da dies ein geeigneter Anwendungsfall ist. Sie können natürlich mit den Informationen, dass in letzter Zeit Aktivitäten stattgefunden haben, tun, was Sie wollen.
Mihai Timar
1
Ich möchte diese Antwort verbessern. Wenn Sie mit Hinweisen helfen können, um es klarer zu machen, wäre dies eine große Hilfe.
Mihai Timar
Ich habe Ihre Erklärung zu Ihrer Antwort hinzugefügt. Es macht jetzt viel mehr Sinn.
Jasper
3

Letztendlich müssen Sie definieren, was Sie als inaktiv betrachten. Ist inaktiv das Ergebnis, dass der Benutzer den Bildschirm nicht berührt, oder ist es der Status des Systems, wenn keine Computerressourcen verwendet werden? In vielen Anwendungen ist es möglich, dass der Benutzer etwas tut, auch wenn er nicht aktiv über den Touchscreen mit dem Gerät interagiert. Während der Benutzer wahrscheinlich mit dem Konzept des Ruhezustands des Geräts und dem Hinweis, dass dies durch Dimmen des Bildschirms geschieht, vertraut ist, ist es nicht unbedingt so, dass er erwartet, dass im Leerlauf etwas passiert - Sie müssen vorsichtig sein darüber, was du tun würdest. Zurück zur ursprünglichen Aussage: Wenn Sie den ersten Fall als Ihre Definition betrachten, gibt es keinen wirklich einfachen Weg, dies zu tun. Sie müssten jedes Berührungsereignis erhalten, Weitergabe an die Antwortkette nach Bedarf unter Angabe der Empfangszeit. Dies gibt Ihnen eine Grundlage für die Berechnung des Leerlaufs. Wenn Sie den zweiten Fall als Ihre Definition betrachten, können Sie mit einer NSPostWhenIdle-Benachrichtigung spielen, um zu versuchen, Ihre Logik zu diesem Zeitpunkt auszuführen.

Wisequark
quelle
1
Zur Verdeutlichung spreche ich über die Interaktion mit dem Bildschirm. Ich werde die Frage aktualisieren, um dies widerzuspiegeln.
Mike McMaster
1
Dann können Sie etwas implementieren, bei dem Sie jedes Mal, wenn eine Berührung auftritt, einen Wert aktualisieren, den Sie überprüfen, oder sogar einen Leerlauf-Timer so einstellen (und zurücksetzen), dass er ausgelöst wird. Sie müssen ihn jedoch selbst implementieren, da, wie Wisequark sagte, das, was Leerlauf ausmacht, zwischen diesen Werten variiert verschiedene Apps.
Louis Gerbarg
1
Ich definiere "Leerlauf" streng als Zeit seit dem letzten Berühren des Bildschirms. Ich verstehe, dass ich es selbst implementieren muss. Ich habe mich nur gefragt, was der "beste" Weg wäre, um beispielsweise Bildschirmberührungen abzufangen, oder ob jemand eine alternative Methode kennt, um dies zu bestimmen.
Mike McMaster
3

Es gibt eine Möglichkeit, diese App breit zu machen, ohne dass einzelne Controller etwas tun müssen. Fügen Sie einfach eine Gestenerkennung hinzu, die Berührungen nicht abbricht. Auf diese Weise werden alle Berührungen für den Timer verfolgt, und andere Berührungen und Gesten werden überhaupt nicht beeinflusst, sodass niemand anderes davon wissen muss.

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

Wenn die Startmethode Ihres App-Delegaten abgeschlossen ist, rufen Sie einfach addGesture auf und schon sind Sie fertig. Alle Berührungen durchlaufen die Methoden von CatchAllGesture, ohne dass die Funktionalität anderer beeinträchtigt wird.

Jlam
quelle
1
Ich mag diesen Ansatz, habe ihn für ein ähnliches Problem mit Xamarin verwendet: stackoverflow.com/a/51727021/250164
Wolfgang Schreurs
1
Funktioniert hervorragend, scheint auch, dass diese Technik verwendet wird, um die Sichtbarkeit von UI-Steuerelementen in AVPlayerViewController ( private API- Referenz ) zu steuern . Das Überschreiben der App -sendEvent:ist übertrieben und behandelt UITrackingRunLoopModenicht viele Fälle.
Roman B.
@ RomanB. ja genau. Wenn Sie lange genug mit iOS gearbeitet haben, wissen Sie, dass Sie immer "den richtigen Weg" verwenden müssen. Dies ist der einfache Weg, um eine benutzerdefinierte Geste als beabsichtigten Entwickler
Jlam
Was bewirkt das Setzen des Status auf .failed in touchEnded?
Stonedauwg
Ich mag diesen Ansatz, aber wenn er ausprobiert wird, scheint er nur Klicks zu fangen, keine andere Geste wie Schwenken, Wischen usw. in Berührungen. War das beabsichtigt?
Stonedauwg