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

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

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

[UIApplication sharedApplication].idleTimerDisabled;

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

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

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

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

Ответы [ 9 ]

152 голосов
/ 21 ноября 2008

Вот ответ, который я искал:

Пусть ваше приложение делегирует подкласс 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");
86 голосов
/ 11 марта 2011

У меня есть вариант решения для таймера простоя, который не требует создания подкласса 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

(код очистки памяти исключен для краткости.)

16 голосов
/ 12 мая 2017

для 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)
12 голосов
/ 14 апреля 2010

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

Вот суть:

http://gist.github.com/365998

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

4 голосов
/ 13 августа 2014

Вот еще один способ обнаружения активности:

Таймер добавлен в 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];
4 голосов
/ 06 февраля 2013

Я только что столкнулся с этой проблемой в игре, которая управляется движениями, т. Е. Заблокирована блокировка экрана, но она должна снова включиться в режиме меню. Вместо таймера я инкапсулировал все вызовы 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 голосов
/ 27 августа 2010

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

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

et voilà!

3 голосов
/ 04 августа 2017

Есть способ сделать это приложение широким, без необходимости выполнять какие-либо действия с отдельными контроллерами. Просто добавьте распознаватель жестов, который не отменяет прикосновения. Таким образом, все прикосновения будут отслеживаться по таймеру, а другие прикосновения и жесты не будут затронуты вообще, поэтому никто не должен знать об этом.

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, не нарушая функциональность других.

3 голосов
/ 08 ноября 2008

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

...