iPhone: Son ekran dokunuşundan bu yana kullanıcı hareketsizliğini / boşta kalma süresini algılama


152

Kullanıcı belirli bir süre boyunca ekrana dokunmazsa, belirli bir işlem yaptığınız bir özellik uygulayan oldu mu? Bunu yapmanın en iyi yolunu bulmaya çalışıyorum.

UIApplication'da bu ile ilgili bir yöntem var:

[UIApplication sharedApplication].idleTimerDisabled;

Bunun yerine böyle bir şey olsaydı iyi olurdu:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Sonra bir zamanlayıcı ayarlayabilir ve bu değeri periyodik olarak kontrol edebilir ve bir eşiği aştığında bazı işlemler yapabilirim.

Umarım bu ne aradığımı açıklar. Bu sorunu daha önce ele almış veya nasıl yapacağınız konusunda düşünceleriniz var mı? Teşekkürler.


Bu harika bir soru. Windows bir OnIdle olayı kavramına sahiptir, ancak uygulamanın şu anda mesaj pompasında sadece cihazı kilitlemekle ilgili görünen iOS idleTimerDisabled özelliğine karşı bir şey işlememesi hakkında daha fazla olduğunu düşünüyorum. İOS / MacOSX'ta Windows konseptine uzaktan bile yakın bir şey olup olmadığını bilen var mı?
stonedauwg

Yanıtlar:


153

İşte aradığım cevap:

Uygulamanızın alt sınıf UIA uygulamasını temsil etmesini sağlayın. Uygulama dosyasında, sendEvent: yöntemini şu şekilde geçersiz kılın:

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

burada maxIdleTime ve idleTimer örnek değişkenleridir.

Bunun çalışması için, main.m'nizi de değiştirerek UIApplicationMain'e ana sınıf olarak delege sınıfınızı (bu örnekte AppDelegate) kullanmasını bildirmeniz gerekir:

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

3
Merhaba Mike, Benim AppDelegate NSObject miras alıyor Yani kullanıcı boşta olduğunu tespit etmek için yukarıdaki yöntemleri UIApplication ve Uygula değişti ama hata alıyorum "Yakalanmayan istisna 'NSInternalInconsistencyException' nedeniyle uygulamayı sonlandırma, nedeni: 'Sadece bir UIApplication örneği olabilir.' “.. yapmam gereken başka bir şey var mı…?
Mihir Mehta


Zamanlayıcılar ateşlemeyi bıraktığında cihazın etkin olmayan duruma girmesiyle nasıl çalışacağından emin değilim?
anonmys

zaman aşımı olayı için popToRootViewController işlevini kullanarak atarsanız düzgün çalışmıyor. UIAlertView, sonra popToRootViewController'ı gösterdiğimde, daha sonra uiviewController'dan bir seçiciyle UIAlertView üzerinde herhangi bir düğmeye basıyorum
Gargo

4
Çok hoş! Ancak, bu yaklaşım NSTimerçok fazla dokunuş varsa çok fazla örnek oluşturur .
Andreas Ley

86

Ben alt sınıflama UIApplication gerektirmeyen boşta zamanlayıcı çözüm bir varyasyonu var. Belirli bir UIViewController alt sınıfında çalışır, bu nedenle yalnızca bir görünüm denetleyiciniz varsa (etkileşimli bir uygulama veya oyun gibi) veya yalnızca belirli bir görünüm denetleyicisinde boşta kalma zaman aşımını işlemek istiyorsanız yararlıdır.

Ayrıca, boşta kalma zamanlayıcısı her sıfırlandığında NSTimer nesnesini yeniden oluşturmaz. Yalnızca zamanlayıcı tetiklenirse yeni bir tane oluşturur.

Kodunuz resetIdleTimer, boşta kalma zamanlayıcısını geçersiz kılması gerekebilecek diğer olayları (önemli ivmeölçer girişi gibi) çağırabilir .

@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

(kısalık için bellek temizleme kodu hariç tutulur.)


1
Çok iyi. Bu cevap sallanıyor! Her ne kadar çok daha erken olduğunu biliyorum ama bu şimdi daha iyi bir çözüm olduğunu doğru olarak işaretlenmiş cevap yener.
Chintan Patel

Bu harika, ama bulduğum bir sorun: UITableViews içinde kaydırma nextResponder çağrılmasına neden olmaz. Ayrıca dokunuşlarla izleme yapmayı denedimBegan: ve dokunuşlarGönderildi: ama hiçbir gelişme yok. Herhangi bir fikir?
Greg Maletic

3
@GregMaletic: i aynı sorunu vardı ama sonunda ekledim - (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView {NSLog (@ "Sürüklemeye başlayacak"); } - (geçersiz) scrollViewDidScroll: (UIScrollView *) scrollView {NSLog (@ "Did Scroll"); [self resetIdleTimer]; } bunu denediniz mi?
Akshay Aher

Teşekkürler. Bu hala yardımcı oluyor. Swift'e taşıdım ve harika çalıştı.
Mark.ewd

sen bir rock yıldızısın. kudos
Pras

21

Swift v 3.1 için

AppDelegate // @ UIApplicationMain'de bu satıra yorum yapmayı unutmayın

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 dosyası oluşturun ve ekleyin (ad önemlidir)

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

Başka bir sınıfta bildirimi gözlemleme

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

2
Neden check- if idleTimer != nilin sendEvent()yöntemine ihtiyacımız olduğunu anlamıyorum ?
Guangyu Wang

timeoutInSecondsWeb hizmeti yanıtından nasıl değer ayarlayabiliriz ?
Kullanıcı_1191

12

Bu iş parçacığı çok yardımcı oldu ve ben bildirimleri gönderen bir UIWindow alt sınıfına tamamladım. Gerçek bir gevşek bağlantı yapmak için bildirimleri seçtim, ancak yeterince kolayca bir temsilci ekleyebilirsiniz.

İşte öz:

http://gist.github.com/365998

Ayrıca, UIApplication alt sınıf sorununun nedeni, NIB'nin uygulama ve delege içerdiği için 2 UIApplication nesnesi oluşturacak şekilde ayarlanmış olmasıdır. UIWindow alt sınıfı harika çalışıyor.


1
bana kodunuzu nasıl kullanacağınızı söyleyebilir misiniz? nasıl denir anlamıyorum
R. Dewi

2
Dokunuşlar için harika çalışıyor, ancak klavyeden gelen girdileri ele almıyor gibi görünüyor. Bu, kullanıcının gui klavyesinde bir şeyler yazıp yazmadığını zaman aşımına uğratacağı anlamına gelir.
Martin Wickman

2
Ben de nasıl kullanılacağını anlayamıyorum ... Ben görünüm kontrolör gözlemciler eklemek ve app dokunulmamış / boş olduğunda b ateş bildirimleri bekliyor .. ama hiçbir şey olmadı ... artı boşta zamanı kontrol edebilirsiniz nerede? 120 saniye sonra boşta kalma süresi istiyorum, böylece 120 saniye sonra IdleNotification ateş gerekir, ondan önce değil.
Ans

5

Aslında alt sınıflandırma fikri harika çalışıyor. Temsilcinizi UIApplicationalt sınıf yapmayın . UIApplicationKaynağından devralınan başka bir dosya oluşturun (ör. MyApp). IB'de, fileOwnernesnenin sınıfını myAppmyApp.m öğesine ayarlayın sendEventve yukarıdaki yöntemi uygulayın. Main.m'de şunları yapın:

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

et voilà!


1
Evet, tek başına bir UIApplication alt sınıfı oluşturmak iyi çalışıyor gibi görünüyor. İkinci parm nil ana bıraktım.
Hot Licks


4

Ben sadece ekran kilidi devre dışı ama menü modundayken tekrar etkinleştirmelidir hareketleri ile kontrol edilen bir oyun ile bu sorunla karşılaştı. Bir zamanlayıcı yerine setIdleTimerDisabled, aşağıdaki yöntemleri sağlayarak küçük bir sınıf içindeki tüm çağrıları kapsadım :

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

disableIdleTimerboşta kalma zamanlayıcısını devre dışı bırakır enableIdleTimerDelayed, menüye girerken veya boşta kalma zamanlayıcısı etkinken çalıştırılması gereken her şeyi devre dışı bırakır ve tüm değişikliklerinizin sistem varsayılan davranışına uygun şekilde sıfırlanmasını sağlamak enableIdleTimeriçin AppDelegate'in applicationWillResignActiveyönteminden çağrılır .
Bir makale yazdım ve iPhone Oyunları'nda IdleTimerManager Idle Timer Handling sınıfının kodunu sağladım


4

Etkinliği tespit etmenin başka bir yolu:

Zamanlayıcı eklenir UITrackingRunLoopMode, böylece yalnızca UITrackingetkinlik varsa tetiklenebilir. Ayrıca, tüm dokunma olayları için sizi spam yapmama gibi güzel bir avantaja sahiptir, böylece son ACTIVITY_DETECT_TIMER_RESOLUTIONsaniyelerde etkinlik olup olmadığını bildirir . Seçiciyi keepAlivebunun için uygun bir kullanım durumu gibi görüyorum . Tabii ki yakın zamanda gerçekleşen bilgilerle istediğinizi yapabilirsiniz.

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

nasıl yani? i "İhtiyacınız olan her şey için kendinizi" keepAlive "seçici yapmak gerekir inanıyorum. Belki bakış açını kaçırıyorum?
Mihai Timar

Bunun etkinliği tespit etmenin başka bir yolu olduğunu söylüyorsunuz, ancak bu sadece bir NSTimer olan bir iVar başlatır. Bunun OP'nin sorusuna nasıl cevap verdiğini anlamıyorum.
Jasper

1
Zamanlayıcı UITrackingRunLoopMode içine eklenir, böylece yalnızca UITracking etkinliği varsa tetiklenebilir. Ayrıca, tüm dokunma etkinlikleri için sizi spam yapmama gibi güzel bir avantaja sahiptir, bu nedenle son ACTIVITY_DETECT_TIMER_RESOLUTION saniyede etkinlik olup olmadığını bildirir. Bunun için uygun bir kullanım durumu gibi göründüğü için keepAlive seçicisini adlandırdım. Tabii ki son zamanlarda aktivite olduğu bilgisiyle istediğiniz her şeyi yapabilirsiniz.
Mihai Timar

1
Bu cevabı geliştirmek istiyorum. Eğer daha net hale getirmek için işaretçilerle yardımcı olabilirseniz, bu çok yardımcı olacaktır.
Mihai Timar

Açıklamanızı cevabınıza ekledim. Şimdi çok daha mantıklı.
Jasper

3

Nihayetinde, boşta olduğunu düşündüğünüz şeyi tanımlamanız gerekir - boşta mı, kullanıcının ekrana dokunmamasının sonucu mu yoksa hiçbir bilgi kaynağı kullanılmıyorsa sistemin durumu mu? Birçok uygulamada, kullanıcının dokunmatik ekran üzerinden cihazla aktif olarak etkileşime girmese bile bir şey yapması mümkündür. Kullanıcı muhtemelen cihazın uyuyacağı kavramını ve ekran karartmasıyla olacağını fark etse de, boşta kaldıklarında bir şey olmasını beklemeleri gerekmemektedir - dikkatli olmanız gerekir ne yapacağınız hakkında. Ancak orijinal ifadeye geri dönersek - ilk vakanın tanımınız olduğunu düşünüyorsanız, bunu yapmanın gerçekten kolay bir yolu yoktur. Her dokunma etkinliğini almanız gerekir, alındığı zamanı not ederken gerektiğinde yanıtlayıcı zincirinden geçirme. Bu size boşta hesaplama yapmak için bazı temeller verecektir. İkinci vakanın sizin tanımınız olduğunu düşünüyorsanız, o zaman mantığınızı gerçekleştirmeyi denemek için bir NSPostWhenIdle bildirimi ile oynayabilirsiniz.


1
Sadece açıklığa kavuşturmak için, ekranla etkileşimden bahsediyorum. Soruyu bunu yansıtacak şekilde güncelleyeceğim.
Mike McMaster

1
Daha sonra, bir dokunuşun gerçekleştiği herhangi bir zamanda, kontrol ettiğiniz bir değeri güncellediğiniz veya hatta boşta bir zamanlayıcıyı ateşlemeye ayarladığınız (ve sıfırladığınız) bir şey uygulayabilirsiniz, ancak wisequark'ın dediği gibi, boşta duran şey, farklı uygulamalar.
Louis Gerbarg

1
Ekrana son dokunmadan beri kesinlikle "boşta" olarak tanımlıyorum. Bunu kendim uygulamak zorunda kalacağımı anladım, sadece ekran dokunuşlarına "en iyi" yolun ne olacağını merak ediyordum ya da birisi bunu belirlemek için alternatif bir yöntem biliyorsa.
Mike McMaster

3

Bireysel denetleyiciler bir şey yapmak zorunda kalmadan bu uygulamayı geniş yapmak için bir yolu var. Sadece dokunuşları iptal etmeyen bir hareket tanıyıcı ekleyin. Bu şekilde, zamanlayıcı için tüm dokunuşlar izlenecek ve diğer dokunuşlar ve hareketler hiç etkilenmeyecek, bu yüzden hiç kimse bunu bilmiyor.

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

Uygulama temsilcinizin tamamlama başlatma yönteminde addGesture'ı arayın ve hazırsınız. Tüm dokunuşlar, başkalarının işlevselliğini engellemeden CatchAllGesture'un yöntemlerinden geçecektir.


1
Bu yaklaşımı beğendim, Xamarin ile benzer bir sorun için kullandım: stackoverflow.com/a/51727021/250164
Wolfgang Schreurs

1
Harika çalışıyor, aynı zamanda bu teknik AVPlayerViewController ( özel API ref ) ' de UI kontrollerinin görünürlüğünü kontrol etmek için kullanılıyor gibi görünüyor . Uygulamayı geçersiz kılma fazladır -sendEvent:ve UITrackingRunLoopModebirçok vakayı ele almaz.
Roman

@RomanB. Evet kesinlikle. iOS ile yeterince uzun süre çalıştığınızda, her zaman "doğru yolu" kullanmayı biliyorsunuzdur ve bu, özel bir
jestin

Durumu .failed olarak ayarlamak dokunuşlarda ne başarır?
stonedauwg

Bu yaklaşımı seviyorum ama denendiğinde sadece muslukları yakalamak gibi görünüyor, dokunmalarda pan, tokatlamak vb.Gibi herhangi bir jest değil. Bu amaçlandı mı?
stonedauwg
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.