iPhone: виявлення бездіяльності / простою користувача з моменту останнього торкання екрана


152

Хтось реалізував функцію, коли, якщо користувач не торкався екрана протягом певного періоду часу, ви вживаєте певних дій? Я намагаюся розібратися, як найкраще це зробити.

Цей дещо пов'язаний метод у застосуванні UIA:

[UIApplication sharedApplication].idleTimerDisabled;

Було б добре, якби натомість у вас було щось подібне:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Тоді я міг налаштувати таймер і періодично перевіряти це значення та вживати певних дій, коли воно перевищує поріг.

Сподіваюсь, це пояснює, що я шукаю. Хтось уже вирішував цю проблему чи має якісь думки щодо того, як би ви це зробили? Дякую.


Це чудове питання. У Windows є концепція події OnIdle, але я думаю, що мова йде більше про те, що додаток зараз нічого не обробляє у своєму повідомленні pump vs iOS idleTimerDisabled, яке, здається, стосується лише блокування пристрою. Хтось знає, чи є щось навіть віддалено близьке до концепції Windows в iOS / MacOSX?
stonedauwg

Відповіді:


153

Ось відповідь, яку я шукав:

Запропонуйте додатку делегувати підклас UIApplication. У файлі реалізації замініть метод sendEvent:

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

де maxIdleTime і idleTimer - змінні екземпляри.

Для того, щоб це працювало, вам також потрібно змінити ваш main.m, щоб повідомити UIApplicationMain використовувати клас делегата (у цьому прикладі AppDelegate) як основний клас:

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

3
Привіт, Майк, мій AppDelegate успадковує NSObject. Тому змінив його UIApplication and Implement вище методи, щоб виявити користувач простоюючим, але я отримую помилку "Завершення програми через невиконаний виняток" NSInternalInconsistencyException ", причина:" Тут може бути лише один екземпляр UIApplication. " ".. ще щось мені потрібно зробити ...?
Міхір Мехта

7
Додам, що підклас UIApplication має бути відокремлений від підкласу
UIApplicationDelegate

Я НЕ впевнений, як це буде працювати з пристроєм, що переходить у неактивний стан, коли таймери перестануть стріляти?
anonmys

не працює належним чином, якщо я призначу використання функції popToRootViewController для події тайм-ауту. Це трапляється, коли я показую UIAlertView, потім popToRootViewController, тоді я натискаю будь-яку кнопку на UIAlertView із селектором з uiviewController, який уже вискочив
Gargo

4
Дуже хороша! Однак такий підхід створює безліч NSTimerвипадків, якщо дотиків багато.
Андреас Лей

86

У мене є варіація рішення простого таймера, яка не вимагає підкласифікації UIA-додатків. Він працює на певному підкласі UIViewController, тому корисний, якщо у вас є лише один контролер перегляду (як, наприклад, інтерактивне додаток або гра), або ви хочете обробляти час очікування простою в певному контролері перегляду.

Він також не створює заново об'єкт NSTimer кожен раз, коли скидається таймер очікування. Він створює новий лише у випадку запуску таймера.

Ваш код може викликати resetIdleTimerбудь-які інші події, які можуть знадобитися недійсним таймером очікування (наприклад, значним входом акселерометра).

@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

(код очищення пам'яті виключено для стислості.)


1
Дуже добре. Ця відповідь хитається! Відповідь позначається правильною, хоча я знаю, що це було набагато раніше, але зараз це краще рішення.
Chintan Patel

Це чудово, але одна проблема, яку я знайшов: прокрутка в UITableViews не викликає виклику nextResponder. Я також спробував відстежувати через touchesBegan: і touchesMoved :, але покращення не було. Будь-які ідеї?
Грег Малетич

3
@GregMaletic: у мене була така ж проблема, але нарешті я додав - (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView {NSLog (@ "Почнеться перетягування"); } - (недійсна) scrollViewDidScroll: (UIScrollView *) scrollView {NSLog (@ "Did Scroll"); [self resetIdleTimer]; } Ви спробували це?
Akshay Aher

Дякую. Це все ще корисно. Я переніс його до Свіфта, і він чудово працював.
Mark.ewd

ти рок-зірка. kudos
Прас

21

Для швидкого v 3.1

не забудьте прокоментувати цей рядок у 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)
   }
} 

створити файл main.swif і додати це (ім'я важливе)

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

Спостереження за будь-яким іншим класом

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

2
Я не розумію , чому нам потрібно перевірити if idleTimer != nilв sendEvent()методі?
Guangyu Wang

Як ми можемо встановити значення для timeoutInSecondsвідповіді веб-служби?
Користувач_1191

12

Цей потік був чудовою підмогою, і я загорнув його в підклас UIWindow, який надсилає сповіщення. Я вибрав сповіщення, щоб зробити його справжнім вільним з'єднанням, але ви можете додати делегата досить легко.

Ось суть:

http://gist.github.com/365998

Також причиною проблеми підкласу UIApplication є те, що NIB налаштовується потім створювати 2 об'єкти UIApplication, оскільки він містить додаток та делегат. Підклас UIWindow чудово працює.


1
Ви можете мені сказати, як користуватися кодом? я не розумію, як це називати
R. Dewi

2
Він чудово працює для дотиків, але, схоже, не справляється із введенням з клавіатури. Це означає, що час вичерпається, якщо користувач набирає речі на клавіатурі gui.
Мартін Вікман

2
Я теж не в змозі зрозуміти, як це використовувати ... Я додаю спостерігачів у контролер перегляду і очікую, що повідомлення буде знято, коли додаток не торкається / не працює ... але нічого не сталося ... плюс звідки ми можемо контролювати час простою? мені подобається, що я хочу простоювати 120 секунд, щоб через 120 секунд IdleNotification повинен запуститись, а не перед цим.
Відповідь

5

Насправді ідея підкласифікації чудово працює. Просто не робіть свого делегата UIApplicationпідкласом. Створіть інший файл, який успадковується UIApplication(наприклад, myApp). В IB встановіть клас fileOwnerоб'єкта myAppі в myApp.m реалізуйте sendEventметод, як зазначено вище. В основному.m зробіть:

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

et voilà!


1
Так, створюється окремий підклас UIApplication, здається, працює добре. Я залишив другий парм нуль в основному.
Гарячі лизання

@Roby, подивись, що мій запит stackoverflow.com/questions/20088521/… .
Тридцять

4

Я просто зіткнувся з цією проблемою з грою, якою керують рухи, тобто заблоковано екран, але слід ввімкнути його знову в режимі меню. Замість таймера я інкапсулював усі дзвінки в setIdleTimerDisabledмежах невеликого класу, пропонуючи такі методи:

- (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деактивує неробочий таймер enableIdleTimerDelayedпри вході в меню або будь-що, що має працювати з активованим таймером очікування, і enableIdleTimerвикликається applicationWillResignActiveметодом вашого AppDelegate, щоб переконатися, що всі ваші зміни належним чином скинуті до поведінки системи за замовчуванням.
Я написав статтю та надав код для одиночного класу IdleTimerManager Idle Timer Handling в iPhone Games


4

Ось ще один спосіб виявлення активності:

Таймер доданий UITrackingRunLoopMode, тому він може запускатись лише за наявності UITrackingактивності. Також є приємна перевага - не спамувати вас на всі сенсорні події, таким чином повідомляючи, чи була активність в останні ACTIVITY_DETECT_TIMER_RESOLUTIONсекунди. Я назвав селектор, keepAliveоскільки це здається відповідним випадком використання для цього. Звичайно, ви можете робити все, що завгодно, з інформацією про те, що нещодавно була діяльність.

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

як так? я вважаю, що зрозуміло, що ви повинні зробити так, щоб ви вибрали себе "seleAlive", на всі потреби. Може, я пропускаю вашу точку зору?
Міхай Тімар

Ви говорите, що це ще один спосіб виявлення активності, однак це лише створює iVar, який є NSTimer. Я не бачу, як це відповідає на питання ОП.
Джаспер

1
Таймер додано в UITrackingRunLoopMode, тому він може запускатись лише у випадку активності UITracking. Також є приємна перевага не спамувати вас на всі торкаються подій, таким чином повідомляючи, чи була активність протягом останніх секунд ACTIVITY_DETECT_TIMER_RESOLUTION. Я назвав селектор KeepAlive, оскільки це здається відповідним випадком використання для цього. Звичайно, ви можете робити все, що завгодно, з інформацією про те, що нещодавно була діяльність.
Mihai Timar

1
Я хотів би вдосконалити цю відповідь. Якщо ви можете допомогти з покажчиками, щоб зробити це зрозумілішим, це буде чудовою допомогою.
Mihai Timar

Я додав ваше пояснення до вашої відповіді. Зараз це має набагато більше сенсу.
Джаспер

3

Зрештою, вам потрібно визначити, що ви вважаєте простою - чи простою є результат, коли користувач не торкається екрана, або це стан системи, якщо не використовуються обчислювальні ресурси? Можливо, у багатьох додатках користувач щось робить, навіть якщо не активно взаємодіє з пристроєм через сенсорний екран. Хоча користувач, мабуть, знайомий з концепцією пристрою, що лягає спати, і помічає, що це станеться через затемнення екрану, це не обов'язково так, що вони очікують, що щось станеться, якщо вони не працюють - потрібно бути обережним про те, що ти би робив. Але повернувшись до початкового твердження - якщо ви вважаєте 1-й випадок своїм визначенням, немає справді простого способу зробити це. Вам потрібно буде отримувати кожен дотик, передаючи його по ланцюгу відповідей за потребою, зазначаючи час його отримання. Це дасть вам певну основу для розрахунку простою. Якщо ви вважаєте, що другий випадок є вашим визначенням, ви можете пограти з повідомленням NSPostWhenIdle, щоб спробувати виконати свою логіку на той час.


1
Просто для уточнення, я говорю про взаємодію з екраном. Я оновлю питання, щоб це відобразити.
Майк МакМастер

1
Тоді ви можете реалізувати щось, коли будь-який момент дотику ви оновлюєте значення, яке ви перевіряєте, або навіть встановлюєте (і скидаєте) неробочий таймер для запуску, але вам потрібно реалізувати його самостійно, оскільки, як сказав мудрець, те, що становить простий, змінюється між різні програми.
Луї Гербарг

1
Я визначаю "простою" строго як час з моменту останнього торкання екрана. Я розумію, що мені потрібно буде це реалізувати самостійно, мені просто було цікаво, яким буде "найкращий" спосіб, скажімо, перехоплювати дотики до екрана, або, якщо хтось знає альтернативний метод для визначення цього.
Майк МакМастер

3

Існує спосіб зробити цю програму широко, без окремих контролерів нічого робити. Просто додайте розпізнавальник жестів, який не скасовує торкання. Таким чином, усі штрихи відстежуватимуться за таймером, а інші штрихи та жести взагалі не впливають, тому ніхто більше не повинен про це знати.

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

У вашому делегаті програми завершено метод запуску, просто зателефонуйте на addGesture і все готово. Усі штрихи пройдуть методами CatchAllGesture, не перешкоджаючи функціональності інших.


1
Мені подобається такий підхід, я використовував його для подібної проблеми з Xamarin: stackoverflow.com/a/51727021/250164
Wolfgang Schreurs

1
Чудово працює, також здається, що ця методика використовується для контролю видимості елементів управління інтерфейсом у AVPlayerViewController (посилання приватного API ). Переважна програма -sendEvent:є надмірною, і UITrackingRunLoopModeвона не обробляє багато випадків.
Роман Б.

@RomanB. так точно. коли ви працюєте з iOS досить довго, ви знаєте, що завжди використовувати "правильний шлях", і це простий спосіб реалізувати користувацький жест за призначенням developer.apple.com/documentation/uikit/uigesturerecognizer/…
Jlam

Що означає встановлення стану на .failed у дотику Закінчено?
stonedauwg

Мені подобається такий підхід, але, коли його спробував, здається, тільки ловить натискання, а не будь-який інший жест, як панорамування, проведіть пальцем тощо у дотикахЗакінчено, куди піде логіка скидання. Це було призначено?
stonedauwg
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.