iPhone: Mendeteksi ketidakaktifan / waktu idle pengguna sejak sentuhan layar terakhir

152

Adakah yang mengimplementasikan fitur di mana jika pengguna belum menyentuh layar selama periode waktu tertentu, Anda mengambil tindakan tertentu? Saya mencoba mencari cara terbaik untuk melakukan itu.

Ada metode yang agak terkait ini dalam aplikasi UIA:

[UIApplication sharedApplication].idleTimerDisabled;

Alangkah baiknya jika Anda memiliki sesuatu seperti ini:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Lalu saya bisa mengatur timer dan secara berkala memeriksa nilai ini, dan mengambil beberapa tindakan ketika melebihi ambang batas.

Semoga itu menjelaskan apa yang saya cari. Adakah yang sudah mengatasi masalah ini, atau memiliki pemikiran tentang bagaimana Anda akan melakukannya? Terima kasih.

Mike McMaster
sumber
Ini pertanyaan yang bagus. Windows memiliki konsep acara OnIdle tapi saya pikir ini lebih tentang aplikasi yang saat ini tidak menangani apa pun di pompa pesannya vs properti idleTimerDisabled iOS yang tampaknya hanya peduli dengan mengunci perangkat. Adakah yang tahu kalau ada sesuatu yang dekat dengan konsep Windows di iOS / MacOSX?
stonedauwg

Jawaban:

153

Inilah jawaban yang saya cari:

Mintalah aplikasi Anda mendelegasikan subkelas aplikasi UIA. Dalam file implementasi, ganti metode sendEvent: seperti:

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

di mana maxIdleTime dan idleTimer adalah variabel instan.

Agar ini berfungsi, Anda juga perlu memodifikasi main.m Anda untuk memberi tahu UIApplicationMain untuk menggunakan kelas delegasi Anda (dalam contoh ini, AppDelegate) sebagai kelas utama:

int retVal = UIApplicationMain(argc, argv, @"AppDelegate", @"AppDelegate");
Mike McMaster
sumber
3
Halo Mike, AppDelegate saya mewarisi dari NSObject. Jadi ubahlah aplikasi UIAplikasi dan Implementasikan metode di atas untuk mendeteksi pengguna menjadi idle tetapi saya mendapatkan kesalahan "Mengakhiri aplikasi karena pengecualian tanpa kecuali 'NSInternalInconsistencyException', alasan: 'Hanya ada satu contoh aplikasi UIA.' ".. adakah hal lain yang perlu saya lakukan ...?
Mihir Mehta
7
Saya ingin menambahkan bahwa subclass UIApplication harus terpisah dari subclass UIApplicationDelegate
boliva
Saya TIDAK yakin bagaimana ini akan bekerja dengan perangkat yang memasuki keadaan tidak aktif ketika timer berhenti menembak?
anonmys
tidak berfungsi dengan baik jika saya menetapkan penggunaan fungsi popToRootViewController untuk acara timeout. Itu terjadi ketika saya menunjukkan UIAlertView, lalu popToRootViewController, lalu saya menekan tombol apa saja di UIAlertView dengan pemilih dari uiviewController yang sudah muncul
Gargo
4
Sangat bagus! Namun, pendekatan ini menciptakan banyak NSTimercontoh jika ada banyak sentuhan.
Andreas Ley
86

Saya memiliki variasi solusi pengatur waktu idle yang tidak memerlukan subkelas aplikasi UIA. Ini bekerja pada subkelas UIViewController tertentu, jadi berguna jika Anda hanya memiliki satu pengontrol tampilan (seperti yang dimiliki aplikasi atau game interaktif) atau hanya ingin menangani waktu tunggu idle dalam pengontrol tampilan tertentu.

Itu juga tidak membuat kembali objek NSTimer setiap kali waktu siaga diatur ulang. Ini hanya membuat yang baru jika timer menyala.

Kode Anda dapat memanggil resetIdleTimeruntuk acara lain yang mungkin perlu membatalkan timer idle (seperti input accelerometer yang signifikan).

@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

(kode pembersihan memori dikecualikan untuk singkatnya.)

Chris Miles
sumber
1
Baik sekali. Jawaban ini mengguncang! Mengalahkan jawaban yang ditandai sebagai benar walaupun saya tahu itu jauh sebelumnya tetapi ini adalah solusi yang lebih baik sekarang.
Chintan Patel
Ini bagus, tetapi satu masalah yang saya temukan: menggulir di UITableViews tidak menyebabkan panggilan berikutnya dipanggil. Saya juga mencoba melacak melalui touches Mulai: dan touches Dipindahkan :, tetapi tidak ada peningkatan. Ada ide?
Greg Maletic
3
@GregMaletic: saya memiliki masalah yang sama tetapi akhirnya saya telah menambahkan - (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView {NSLog (@ "Akan mulai menyeret"); } - (void) scrollViewDidScroll: (UIScrollView *) scrollView {NSLog (@ "Did Scroll"); [self resetIdleTimer]; } Sudahkah Anda mencoba ini?
Akshay Aher
Terima kasih. Ini masih membantu. Saya memindahkannya ke Swift dan bekerja dengan sangat baik.
Mark.ewd
Anda seorang rockstar. kudos
Pras
21

Untuk swift v 3.1

jangan lupa komentar baris ini di 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)
   }
} 

buat file main.swif dan tambahkan ini (nama itu penting)

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

Mengamati notifikasi di kelas lain mana pun

NotificationCenter.default.addObserver(self, selector: #selector(someFuncitonName), name: Notification.Name.TimeOutUserInteraction, object: nil)
Sergey Stadnik
sumber
2
Saya tidak mengerti mengapa kami membutuhkan metode check- if idleTimer != nilin sendEvent()?
Guangyu Wang
Bagaimana kita dapat menetapkan nilai timeoutInSecondsdari respons layanan web?
User_1191
12

Utas ini sangat membantu, dan saya membungkusnya menjadi subkelas UIWindow yang mengirimkan pemberitahuan. Saya memilih pemberitahuan untuk menjadikannya kopling longgar yang nyata, tetapi Anda dapat menambahkan delegasi dengan cukup mudah.

Inilah intinya:

http://gist.github.com/365998

Selain itu, alasan untuk masalah subkelas UIApplication adalah bahwa NIB diatur untuk kemudian membuat 2 objek aplikasi UIApplication karena berisi aplikasi dan delegasi. Subkelas UIWindow berfungsi dengan baik.

Brian King
sumber
1
dapatkah Anda memberi tahu saya cara menggunakan kode Anda? saya tidak mengerti bagaimana menyebutnya
R. Dewi
2
Ini berfungsi bagus untuk sentuhan, tetapi tampaknya tidak menangani input dari keyboard. Ini berarti akan habis jika pengguna mengetik barang di keyboard gui.
Martin Wickman
2
Saya juga tidak bisa mengerti cara menggunakannya ... Saya menambahkan pengamat di pengontrol tampilan saya dan mengharapkan pemberitahuan untuk b dipecat ketika aplikasi tidak tersentuh / idle .. tapi tidak ada yang terjadi ... ditambah dari mana kita dapat mengontrol waktu idle? seperti saya ingin waktu idle 120 detik sehingga setelah 120 detik IdleNotification harus diaktifkan, bukan sebelum itu.
Ans
5

Sebenarnya ide subclassing bekerja dengan baik. Hanya saja, jangan membuat Anda mendelegasikan UIApplicationsubclass. Buat file lain yang mewarisi dari UIApplication(mis. MyApp). Di IB atur kelas fileOwnerobjek ke myAppdan di myApp.m terapkan sendEventmetode seperti di atas. Di main.m lakukan:

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

dan lagi!

Roby
sumber
1
Yap, membuat subkelas aplikasi UIA yang berdiri sendiri tampaknya berfungsi dengan baik. Saya meninggalkan parm nil kedua di main.
Hot Licks
@Roby, lihat permintaan saya stackoverflow.com/questions/20088521/… .
Tirth
4

Saya baru saja mengalami masalah ini dengan permainan yang dikendalikan oleh gerakan yaitu kunci layar dinonaktifkan tetapi harus mengaktifkannya lagi ketika dalam mode menu. Alih-alih penghitung waktu saya merangkum semua panggilan ke setIdleTimerDisableddalam kelas kecil menyediakan metode berikut:

- (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 menonaktifkan timer idle, enableIdleTimerDelayed ketika memasuki menu atau apa pun yang harus dijalankan dengan timer idle aktif dan enableIdleTimerdipanggil dari applicationWillResignActivemetode AppDelegate Anda untuk memastikan semua perubahan Anda diatur ulang dengan benar ke perilaku default sistem.
Saya menulis sebuah artikel dan memberikan kode untuk Penanganan Timer IdleTimerManager Idle kelas singleton di Game iPhone

Kay
sumber
4

Berikut cara lain untuk mendeteksi aktivitas:

Timer ditambahkan UITrackingRunLoopMode, sehingga hanya bisa menyala jika ada UITrackingaktivitas. Ini juga memiliki keuntungan bagus untuk tidak mengirim spam kepada Anda untuk semua acara sentuh, sehingga memberi tahu jika ada aktivitas di ACTIVITY_DETECT_TIMER_RESOLUTIONdetik-detik terakhir . Saya menamai pemilih tersebut keepAlivekarena tampaknya merupakan use case yang sesuai untuk ini. Anda tentu saja dapat melakukan apa pun yang Anda inginkan dengan informasi yang ada kegiatan baru-baru ini.

_touchesTimer = [NSTimer timerWithTimeInterval:ACTIVITY_DETECT_TIMER_RESOLUTION
                                        target:self
                                      selector:@selector(keepAlive)
                                      userInfo:nil
                                       repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_touchesTimer forMode:UITrackingRunLoopMode];
Mihai Timar
sumber
bagaimana? Saya percaya itu jelas Anda harus membuat Anda "keepAlive" pemilih sendiri untuk apa pun kebutuhan Anda. Mungkin saya kehilangan sudut pandang Anda?
Mihai Timar
Anda mengatakan bahwa ini adalah cara lain untuk mendeteksi aktivitas, namun, ini hanya instantiate iVar yang merupakan NSTimer. Saya tidak melihat bagaimana ini menjawab pertanyaan OP.
Jasper
1
Timer ditambahkan dalam UITrackingRunLoopMode, sehingga hanya bisa menyala jika ada aktivitas UITracking. Ini juga memiliki keuntungan yang bagus yaitu tidak mengirim spam kepada Anda untuk semua acara sentuh, sehingga memberi tahu jika ada aktivitas dalam ACTIVITY_DETECT_TIMER_RESOLUTION detik yang lalu. Saya menamai pemilih keepAlive karena sepertinya ini adalah use case yang sesuai untuk ini. Anda tentu saja dapat melakukan apa pun yang Anda inginkan dengan informasi yang ada kegiatan baru-baru ini.
Mihai Timar
1
Saya ingin meningkatkan jawaban ini. Jika Anda dapat membantu dengan petunjuk tentang membuatnya lebih jelas itu akan sangat membantu.
Mihai Timar
Saya menambahkan penjelasan Anda ke jawaban Anda. Lebih masuk akal sekarang.
Jasper
3

Pada akhirnya Anda perlu mendefinisikan apa yang Anda anggap sebagai idle - apakah ini hasil dari pengguna yang tidak menyentuh layar atau apakah itu keadaan sistem jika tidak ada sumber daya komputasi yang digunakan? Dimungkinkan, dalam banyak aplikasi, bagi pengguna untuk melakukan sesuatu walaupun tidak secara aktif berinteraksi dengan perangkat melalui layar sentuh. Sementara pengguna mungkin akrab dengan konsep perangkat yang akan tidur dan pemberitahuan bahwa itu akan terjadi melalui peredupan layar, belum tentu mereka akan mengharapkan sesuatu terjadi jika mereka menganggur - Anda harus berhati-hati tentang apa yang akan Anda lakukan. Tetapi kembali ke pernyataan asli - jika Anda menganggap kasus ke-1 sebagai definisi Anda, tidak ada cara yang sangat mudah untuk melakukan ini. Anda harus menerima setiap acara sentuhan, meneruskannya pada rantai responden sesuai kebutuhan sambil mencatat waktu itu diterima. Itu akan memberi Anda beberapa dasar untuk membuat perhitungan idle. Jika Anda menganggap kasus kedua sebagai definisi Anda, Anda dapat bermain dengan notifikasi NSPostWhenIdle untuk mencoba dan melakukan logika Anda pada saat itu.

bijaksana
sumber
1
Hanya untuk memperjelas, saya berbicara tentang interaksi dengan layar. Saya akan memperbarui pertanyaan untuk mencerminkan itu.
Mike McMaster
1
Kemudian Anda dapat mengimplementasikan sesuatu ketika terjadi sentuhan, Anda memperbarui nilai yang Anda periksa, atau bahkan mengatur (dan mengatur ulang) timer idle untuk diaktifkan, tetapi Anda perlu menerapkannya sendiri, karena seperti kata bijak kata, apa yang merupakan idle bervariasi antara aplikasi yang berbeda.
Louis Gerbarg
1
Saya mendefinisikan "idle" hanya sebagai waktu sejak terakhir menyentuh layar. Saya mengerti bahwa saya perlu mengimplementasikannya sendiri, saya hanya ingin tahu apa cara "terbaik" untuk, katakanlah, menyadap sentuhan layar, atau jika seseorang mengetahui metode alternatif untuk menentukan ini.
Mike McMaster
3

Ada cara untuk melakukan aplikasi ini secara luas tanpa pengontrol individu harus melakukan apa pun. Cukup tambahkan pengenal isyarat yang tidak membatalkan sentuhan. Dengan cara ini, semua sentuhan akan dilacak untuk timer, dan sentuhan dan gerakan lainnya tidak terpengaruh sama sekali sehingga tidak ada orang lain yang tahu tentang itu.

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

Di delegasi aplikasi Anda, lakukan metode peluncuran selesai, panggil saja addGesture dan Anda sudah siap. Semua sentuhan akan melalui metode CatchAllGesture tanpa mencegah fungsi orang lain.

Jlam
sumber
1
Saya suka pendekatan ini, menggunakannya untuk masalah serupa dengan Xamarin: stackoverflow.com/a/51727021/250164
Wolfgang Schreurs
1
Berfungsi bagus, juga sepertinya teknik ini digunakan untuk mengontrol visibilitas kontrol UI di AVPlayerViewController ( private API ref ) Aplikasi utama terlalu -sendEvent:banyak, dan UITrackingRunLoopModetidak menangani banyak kasus.
Roman B.
@RomanB. ya, tepat sekali. ketika Anda telah bekerja dengan iOS cukup lama, Anda tahu untuk selalu menggunakan "cara yang benar" dan ini adalah cara langsung untuk menerapkan isyarat khusus seperti yang dimaksudkan developer.apple.com/documentation/uikit/uigesturerecognizer/…
Jlam
Apa yang diatur oleh negara untuk mencapai kegagalan dalam sentuhan?
stonedauwg
Saya suka pendekatan ini tetapi ketika mencobanya sepertinya hanya menangkap keran, tidak ada gerakan lain seperti pan, geser dll di touchEnded di mana logika reset akan pergi. Apakah itu dimaksudkan?
stonedauwg