iPhone: Wykrywanie braku aktywności / czasu bezczynności użytkownika od ostatniego dotknięcia ekranu

152

Czy ktoś zaimplementował funkcję, w której jeśli użytkownik nie dotyka ekranu przez określony czas, wykonujesz określoną czynność? Próbuję znaleźć najlepszy sposób na zrobienie tego.

W UIApplication jest ta nieco powiązana metoda:

[UIApplication sharedApplication].idleTimerDisabled;

Byłoby miło, gdybyś zamiast tego miał coś takiego:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Następnie mógłbym ustawić licznik czasu i okresowo sprawdzać tę wartość i podejmować działania, gdy przekroczy próg.

Mam nadzieję, że to wyjaśnia, czego szukam. Czy ktoś już zajął się tym problemem lub ma jakieś przemyślenia, jak byś to zrobił? Dzięki.

Mike McMaster
źródło
To świetne pytanie. System Windows ma koncepcję zdarzenia OnIdle, ale myślę, że bardziej chodzi o aplikację, która obecnie nie obsługuje niczego w swojej pompie komunikatów, a nie na właściwości idleTimerDisabled systemu iOS, która wydaje się być związana tylko z blokowaniem urządzenia. Czy ktoś wie, czy w iOS / MacOSX jest coś, co jest nawet bliskie koncepcji Windows?
stonedauwg

Odpowiedzi:

153

Oto odpowiedź, której szukałem:

Miej podklasę delegata aplikacji UIApplication. W pliku implementacji nadpisz metodę sendEvent: w następujący sposób:

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

gdzie maxIdleTime i idleTimer są zmiennymi instancji.

Aby to zadziałało, musisz również zmodyfikować plik main.m tak, aby UIApplicationMain używał Twojej klasy delegata (w tym przykładzie AppDelegate) jako klasy głównej:

int retVal = UIApplicationMain(argc, argv, @"AppDelegate", @"AppDelegate");
Mike McMaster
źródło
3
Cześć Mike, Moja AppDelegate dziedziczy z NSObject, więc zmieniłem to UIApplication i Implementuj powyższe metody, aby wykryć bezczynność użytkownika, ale otrzymuję błąd „Zamykanie aplikacji z powodu nieprzechwyconego wyjątku„ NSInternalInconsistencyException ”, powód:„ Może istnieć tylko jedno wystąpienie UIApplication ”. „… czy muszę coś jeszcze zrobić…?
Mihir Mehta
7
Dodam, że podklasa UIApplication powinna być odrębna od podklasy
UIApplicationDelegate
NIE jestem pewien, jak to zadziała, gdy urządzenie przejdzie w stan nieaktywny, gdy liczniki przestaną działać?
anonmys
nie działa poprawnie, jeśli przypiszę użycie funkcji popToRootViewController dla zdarzenia limitu czasu. Dzieje się tak, gdy pokazuję UIAlertView, następnie popToRootViewController, a następnie naciskam dowolny przycisk na UIAlertView z selektorem z uiviewController, który jest już uruchomiony
Gargo
4
Bardzo dobrze! Jednak takie podejście tworzy wiele NSTimerprzypadków, jeśli jest dużo akcentów.
Andreas Ley
86

Mam odmianę rozwiązania licznika czasu bezczynności, która nie wymaga podklasy UIApplication. Działa na określonej podklasie UIViewController, więc jest przydatny, jeśli masz tylko jeden kontroler widoku (taki jak interaktywna aplikacja lub gra) lub chcesz tylko obsłużyć limit czasu bezczynności w określonym kontrolerze widoku.

Nie odtwarza również obiektu NSTimer za każdym razem, gdy licznik czasu bezczynności jest resetowany. Tworzy nowy tylko wtedy, gdy timer odpala.

Twój kod może wywoływać resetIdleTimerinne zdarzenia, które mogą wymagać unieważnienia licznika czasu bezczynności (takie jak znaczące wejście akcelerometru).

@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

(kod czyszczenia pamięci został wykluczony ze względu na zwięzłość).

Chris Miles
źródło
1
Bardzo dobrze. Ta odpowiedź rządzi! Pokonuje odpowiedź oznaczoną jako poprawną, chociaż wiem, że to było dużo wcześniej, ale teraz jest to lepsze rozwiązanie.
Chintan Patel
To świetnie, ale znalazłem jeden problem: przewijanie w UITableViews nie powoduje wywołania nextResponder. Próbowałem również śledzić za pomocą dotknięćBegan: i touchMoved :, ale bez poprawy. Jakieś pomysły?
Greg Maletic,
3
@GregMaletic: miałem ten sam problem, ale w końcu dodałem - (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView {NSLog (@ "Zacznie przeciągać"); } - (void) scrollViewDidScroll: (UIScrollView *) scrollView {NSLog (@ "Did Scroll"); [self resetIdleTimer]; } próbowałeś tego?
Akshay Aher
Dzięki. To jest nadal pomocne. Przeniosłem go na Swift i działał świetnie.
Mark.ewd
jesteś gwiazdą rocka. kudos
Pras
21

Dla Swift v 3.1

nie zapomnij skomentować tej linii w 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)
   }
} 

utwórz plik main.swif i dodaj go (nazwa jest ważna)

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

Obserwowanie powiadomień w dowolnej innej klasie

NotificationCenter.default.addObserver(self, selector: #selector(someFuncitonName), name: Notification.Name.TimeOutUserInteraction, object: nil)
Sergey Stadnik
źródło
2
Nie rozumiem, dlaczego musimy czek if idleTimer != nilw sendEvent()metodzie?
Guangyu Wang,
Jak możemy ustawić wartość timeoutInSecondsz odpowiedzi usługi internetowej?
User_1191
12

Ten wątek był bardzo pomocny i zawarłem go w podklasę UIWindow, która wysyła powiadomienia. Wybrałem powiadomienia, aby było to naprawdę luźne powiązanie, ale możesz łatwo dodać delegata.

Oto sedno:

http://gist.github.com/365998

Ponadto przyczyną problemu z podklasą UIApplication jest to, że NIB jest skonfigurowany do tworzenia 2 obiektów UIApplication, ponieważ zawiera aplikację i delegata. Podklasa UIWindow działa jednak świetnie.

Brian King
źródło
1
czy możesz mi powiedzieć, jak używać swojego kodu? nie rozumiem, jak to nazwać
R. Dewi
2
Działa świetnie w przypadku dotknięć, ale wydaje się, że nie obsługuje danych wejściowych z klawiatury. Oznacza to, że upłynie limit czasu, jeśli użytkownik będzie pisać coś na klawiaturze GUI.
Martin Wickman,
2
Ja też nie jestem w stanie zrozumieć, jak go używać ... Dodam obserwatorów do mojego kontrolera widoku i spodziewam się, że powiadomienia będą uruchamiane, gdy aplikacja jest nietknięta / bezczynna ... ale nic się nie stało ... plus skąd możemy kontrolować czas bezczynności? tak jak chcę, aby czas bezczynności wynosił 120 sekund, aby po 120 sekundach IdleNotification było uruchamiane, a nie wcześniej.
Ans
5

Właściwie pomysł na podklasy działa świetnie. Po prostu nie ustawiaj delegata w UIApplicationpodklasie. Utwórz inny plik, który dziedziczy po UIApplication(np. MyApp). W IB ustaw klasę fileOwnerobiektu na, myAppaw myApp. M zaimplementuj sendEventmetodę jak powyżej. W main.m do:

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

zrobione!

Roby
źródło
1
Tak, tworzenie samodzielnej podklasy UIApplication wydaje się działać dobrze. Pozostawiłem drugi parm zero w głównym.
Hot Licks
@Roby, spójrz, że moje zapytanie stackoverflow.com/questions/20088521/… .
Tirth,
4

Właśnie napotkałem ten problem z grą, która jest kontrolowana przez ruchy, tj. Ma wyłączoną blokadę ekranu, ale powinna włączyć ją ponownie w trybie menu. Zamiast timera zawarłem wszystkie wywołania setIdleTimerDisabledw małej klasie, zapewniając następujące metody:

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

disableIdleTimerdezaktywuje licznik czasu bezczynności, enableIdleTimerDelayedgdy wchodzisz do menu lub cokolwiek powinno działać z aktywnym licznikiem czasu bezczynności i enableIdleTimerjest wywoływane z applicationWillResignActivemetody AppDelegate, aby zapewnić, że wszystkie zmiany zostaną poprawnie zresetowane do domyślnego zachowania systemu.
Napisałem artykuł i podałem kod pojedynczej klasy IdleTimerManager Idle Timer Handling w grach na iPhone'a

Okej
źródło
4

Oto inny sposób wykrywania aktywności:

Timer jest dodawany UITrackingRunLoopMode, więc może działać tylko wtedy, gdy jest UITrackingaktywność. Ma również tę zaletę, że nie spamuje Cię w przypadku wszystkich zdarzeń dotykowych, informując w ten sposób, czy w ciągu ostatnich ACTIVITY_DETECT_TIMER_RESOLUTIONsekund była aktywność . Nazwałam selektor, keepAliveponieważ wydaje się to odpowiednim przypadkiem użycia. Możesz oczywiście robić, co chcesz, z informacją, że ostatnio miała miejsce aktywność.

_touchesTimer = [NSTimer timerWithTimeInterval:ACTIVITY_DETECT_TIMER_RESOLUTION
                                        target:self
                                      selector:@selector(keepAlive)
                                      userInfo:nil
                                       repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_touchesTimer forMode:UITrackingRunLoopMode];
Mihai Timar
źródło
jak to? wierzę, że jest jasne, że powinieneś sam decydować o swoich potrzebach. Może brakuje mi twojego punktu widzenia?
Mihai Timar
Mówisz, że jest to inny sposób wykrywania aktywności, jednak to tylko tworzy instancję iVar, która jest NSTimerem. Nie rozumiem, jak to odpowiada na pytanie OP.
Jasper
1
Licznik czasu jest dodawany w UITrackingRunLoopMode, więc może uruchamiać się tylko wtedy, gdy występuje aktywność UITracking. Ma również tę zaletę, że nie spamuje Cię przy wszystkich zdarzeniach dotykowych, informując w ten sposób, czy w ciągu ostatnich ACTIVITY_DETECT_TIMER_RESOLUTION sekund była aktywność. Nazwałem selektor keepAlive, ponieważ wydaje się to odpowiednim przypadkiem użycia. Możesz oczywiście robić, co chcesz, z informacją, że ostatnio miała miejsce aktywność.
Mihai Timar
1
Chciałbym poprawić tę odpowiedź. Jeśli możesz pomóc ze wskazówkami, aby uczynić to jaśniejszym, byłoby to bardzo pomocne.
Mihai Timar
Dodałem twoje wyjaśnienie do twojej odpowiedzi. Teraz ma to o wiele więcej sensu.
Jasper
3

Ostatecznie musisz zdefiniować, co uważasz za bezczynne - czy bezczynność wynika z tego, że użytkownik nie dotyka ekranu, czy też jest to stan systemu, jeśli nie są używane żadne zasoby obliczeniowe? W wielu aplikacjach użytkownik może coś robić, nawet jeśli nie wchodzi w aktywną interakcję z urządzeniem za pośrednictwem ekranu dotykowego. Chociaż użytkownik prawdopodobnie jest zaznajomiony z koncepcją zasypiania urządzenia i zauważa, że ​​stanie się to poprzez przyciemnianie ekranu, niekoniecznie jest tak, że spodziewa się, że coś się stanie, jeśli będzie bezczynny - trzeba być ostrożnym o tym, co byś zrobił. Ale wracając do pierwotnego stwierdzenia - jeśli uznasz pierwszy przypadek za swoją definicję, nie ma naprawdę łatwego sposobu, aby to zrobić. Musisz otrzymywać każde zdarzenie dotykowe, przekazywanie go w razie potrzeby w łańcuchu odpowiedzi, jednocześnie odnotowując czas jego otrzymania. To da ci podstawę do wykonania bezczynnych obliczeń. Jeśli uważasz, że drugi przypadek jest twoją definicją, możesz grać z powiadomieniem NSPostWhenIdle, aby spróbować wykonać swoją logikę w tym czasie.

mądry kwark
źródło
1
Dla wyjaśnienia mówię o interakcji z ekranem. Zaktualizuję pytanie, aby to odzwierciedlić.
Mike McMaster
1
Następnie możesz zaimplementować coś, w którym za każdym razem, gdy zdarzy się dotknięcie, zaktualizujesz wartość, którą sprawdzasz, lub nawet ustawisz (i zresetujesz) licznik czasu bezczynności do uruchomienia, ale musisz to zaimplementować samodzielnie, ponieważ jak powiedział mądryQuark, to, co stanowi bezczynność, różni się między różne aplikacje.
Louis Gerbarg,
1
Termin „bezczynność” definiuję ściśle jako czas od ostatniego dotknięcia ekranu. Rozumiem, że będę musiał to zaimplementować samodzielnie, zastanawiałem się tylko, jaki byłby „najlepszy” sposób, powiedzmy, przechwytywania dotknięć ekranu lub czy ktoś zna alternatywną metodę ustalenia tego.
Mike McMaster
3

Jest sposób na zrobienie tej aplikacji bez konieczności wykonywania jakichkolwiek czynności przez poszczególne kontrolery. Po prostu dodaj rozpoznawanie gestów, który nie anuluje dotknięć. W ten sposób wszystkie dotknięcia będą śledzone dla timera, a inne dotknięcia i gesty w ogóle nie będą miały wpływu, więc nikt inny nie musi o tym wiedzieć.

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

W przypadku, gdy delegat aplikacji zakończył metodę uruchamiania, po prostu wywołaj addGesture i gotowe. Wszystkie dotknięcia przechodzą przez metody CatchAllGesture bez przeszkadzania w działaniu innych.

Jlam
źródło
1
Podoba mi się to podejście, użyłem go do podobnego problemu z Xamarin: stackoverflow.com/a/51727021/250164
Wolfgang Schreurs
1
Działa świetnie, wydaje się też, że ta technika jest używana do kontrolowania widoczności kontrolek interfejsu użytkownika w AVPlayerViewController ( prywatny ref API ). Zastępowanie aplikacji -sendEvent:jest przesadą i UITrackingRunLoopModenie obsługuje wielu przypadków.
Roman B.
@RomanB. tak, dokładnie. jeśli pracujesz z iOS wystarczająco długo, wiesz, że zawsze używasz „we właściwy sposób” i jest to prosty sposób na zaimplementowanie niestandardowego gestu zgodnie z zamierzeniami developer.apple.com/documentation/uikit/uigesturerecognizer/…
Jlam
Co powoduje ustawienie stanu .failed w touchchesEnded?
stonedauwg
Podoba mi się to podejście, ale kiedy próbowałem, wydaje się, że łapie tylko stuknięcia, a nie inne gesty, takie jak przesuwanie, przesuwanie itp. W dotknięciach Zakończone tam, gdzie pójdzie logika resetowania. Czy to było zamierzone?
stonedauwg