iPhone: обнаружение бездействия пользователя / простоя с момента последнего касания экрана

152

Кто-нибудь реализовал функцию, при которой, если пользователь не касался экрана в течение определенного периода времени, вы предпринимаете определенные действия? Я пытаюсь найти лучший способ сделать это.

В UIApplication есть несколько похожий метод:

[UIApplication sharedApplication].idleTimerDisabled;

Было бы неплохо, если бы у вас было что-то вроде этого:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Затем я мог бы установить таймер и периодически проверять это значение и предпринимать некоторые действия, когда оно превышает пороговое значение.

Надеюсь, это объясняет, что я ищу. Кто-нибудь уже занимался этой проблемой, или есть мысли о том, как бы вы это сделали? Спасибо.

Майк МакМастер
источник
Это большой вопрос. В Windows есть концепция события OnIdle, но я думаю, что это больше о приложении, которое в настоящее время не обрабатывает что-либо в своем насосе сообщений, по сравнению со свойством 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 и внедряю вышеуказанные методы, чтобы обнаружить, что пользователь бездействует, но я получаю ошибку «Завершение приложения из-за необработанного исключения« NSInternalInconsistencyException », причина:« Может быть только один экземпляр UIApplication ». "... что-нибудь еще мне нужно сделать ...?
Михир Мехта
7
Я хотел бы добавить, что подкласс UIApplication должен быть отделен от подкласса
UIApplicationDelegate
Я НЕ уверен, как это будет работать с устройством, переходящим в неактивное состояние, когда таймеры прекращают работу?
anonmys
не работает должным образом, если я назначил использование функции popToRootViewController для события тайм-аута. Это происходит, когда я показываю UIAlertView, затем popToRootViewController, затем нажимаю любую кнопку на UIAlertView с помощью селектора из uiviewController, который уже
активирован
4
Очень хорошо! Тем не менее, этот подход создает много NSTimerслучаев, если есть много касаний.
Андреас Лей
86

У меня есть вариант решения таймера простоя, который не требует создания подклассов UIApplication. Он работает с определенным подклассом 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
Очень хорошо. Этот ответ потрясающий! Превосходит ответ, помеченный как правильный, хотя я знаю, что это было намного раньше, но сейчас это лучшее решение.
Чинтан Патель
Это замечательно, но я обнаружил одну проблему: прокрутка в UITableViews не приводит к вызову nextResponder. Я также пытался отслеживать с помощью touchesBegan: и touchesMoved:, но без улучшений. Любые идеи?
Грег Малетик
3
@GregMaletic: у меня была та же проблема, но наконец я добавил - (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView {NSLog (@ "Начнет перетаскивание"); } - (void) scrollViewDidScroll: (UIScrollView *) scrollView {NSLog (@ "Did Scroll"); [self resetIdleTimer]; } ты пробовал это?
Акшай Ахер
Спасибо. Это все еще полезно. Я портировал его на Swift, и он отлично работал.
Mark.ewd
ты рок-звезда. слава
Прас
21

Для Swift 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()методе?
Гуаню Ван
Как мы можем установить значение для timeoutInSecondsответа веб-службы?
User_1191
12

Этот поток очень помог, и я поместил его в подкласс UIWindow, который отправляет уведомления. Я выбрал уведомления, чтобы сделать их по-настоящему слабой связью, но вы можете легко добавить делегата.

Вот суть:

http://gist.github.com/365998

Кроме того, причина проблемы подкласса UIApplication заключается в том, что NIB настроен на создание 2 объектов UIApplication, поскольку он содержит приложение и делегат. Подкласс UIWindow прекрасно работает, хотя.

Брайан Кинг
источник
1
Можете ли вы сказать мне, как использовать свой код? я не понимаю, как это назвать
Р. Деви
2
Он отлично работает для прикосновений, но, похоже, не обрабатывает ввод с клавиатуры. Это означает, что время ожидания истечет, если пользователь набирает текст на клавиатуре.
Мартин Уикман,
2
Я тоже не в состоянии понять, как его использовать ... Я добавляю наблюдателей в свой контроллер представления и ожидаю, что уведомления будут запущены, когда приложение не тронуто / неактивно ... но ничего не произошло ... плюс откуда мы можем контролировать время простоя? как я хочу, чтобы время простоя составляло 120 секунд, чтобы через 120 секунд IdleNotification запускалось, а не до этого.
Ответ
5

На самом деле идея создания подклассов прекрасно работает. Только не делайте своего делегата UIApplicationподклассом. Создайте другой файл, который наследуется от UIApplication(например, myApp). В IB установите класс fileOwnerобъекта myAppи в myApp.m реализуйте sendEventметод, как указано выше. В main.m сделать:

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

и вуаля!

Roby
источник
1
Да, создание автономного подкласса UIApplication, кажется, работает хорошо. Я оставил второй номер ноль в основном.
Hot Licks
@Roby, взгляни, что мой запрос stackoverflow.com/questions/20088521/… .
Tirth
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 Обработка таймера простоя в играх iPhone

Кей
источник
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];
Михай Тимар
источник
как так? Я действительно считаю, что вы должны сами выбирать «keepAlive» для любых нужд. Может быть, я скучаю по вашей точке зрения?
Михай Тимар
Вы говорите, что это еще один способ обнаружения активности, однако он только создает экземпляр iVar, который является NSTimer. Я не понимаю, как это отвечает на вопрос ОП.
Джаспер
1
Таймер добавляется в UITrackingRunLoopMode, поэтому он может срабатывать только при наличии активности UITracking. Это также имеет приятное преимущество - не спамить вас на всех событиях касания, таким образом информируя, если была активность в последние ACTIVITY_DETECT_TIMER_RESOLUTION секунды. Я назвал селектор keepAlive, так как это кажется подходящим вариантом использования для этого. Разумеется, вы можете делать все, что пожелаете, с информацией о том, что было в последнее время
Михай Тимар
1
Я хотел бы улучшить этот ответ. Если вы можете помочь с указателями на прояснение, это было бы очень полезно.
Михай Тимар
Я добавил ваше объяснение к вашему ответу. Теперь это имеет больше смысла.
Джаспер
3

В конечном итоге вам нужно определить, что вы считаете бездействующим - является ли бездействие результатом того, что пользователь не касается экрана, или это состояние системы, если не используются вычислительные ресурсы? Во многих приложениях пользователь может что-то делать, даже если он не взаимодействует с устройством через сенсорный экран. Хотя пользователь, вероятно, знаком с концепцией перехода устройства в режим сна и замечанием того, что это произойдет с помощью затемнения экрана, он не обязательно ожидает, что что-то произойдет, если он находится в режиме ожидания - вам нужно быть осторожным о том, что вы будете делать. Но вернемся к первоначальному утверждению - если вы считаете, что ваш первый случай является вашим определением, то не существует простого способа сделать это. Вам нужно будет получать каждое сенсорное событие, передавая его по цепочке респондента по мере необходимости, отмечая время его получения. Это даст вам некоторую основу для расчета простоя. Если вы рассматриваете второй случай как свое определение, вы можете поиграть с уведомлением NSPostWhenIdle, чтобы попытаться выполнить свою логику в это время.

wisequark
источник
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, не мешая функционированию других.

Jlam
источник
1
Мне нравится этот подход, я использовал его для аналогичной проблемы с Xamarin: stackoverflow.com/a/51727021/250164
Вольфганг Шреурс
1
Прекрасно работает, также кажется, что этот метод используется для управления видимостью элементов управления пользовательского интерфейса в AVPlayerViewController ( частный API ref ). Переопределение приложения -sendEvent:является излишним, и UITrackingRunLoopModeне обрабатывает много случаев.
Роман Б.
@RomanB. Да, именно. Когда вы достаточно долго работали с iOS, вы знаете, что всегда должны использовать «правильный путь», и это простой способ реализовать собственный жест, как и задумано developer.apple.com/documentation/uikit/uigesturerecognizer/…
Jlam
Что делает установка состояния .failed в штрих-закруглениях?
stonedauwg
Мне нравится этот подход, но при попытке он, кажется, ловит только нажатия, а не любые другие жесты, такие как панорамирование, пролистывание и т. Д. В прикосновениях. Это было предназначено?
stonedauwg