iPhone: Phát hiện người dùng không hoạt động / thời gian rảnh kể từ lần chạm màn hình cuối cùng


152

Có ai đã triển khai một tính năng trong đó nếu người dùng không chạm vào màn hình trong một khoảng thời gian nhất định, bạn có thực hiện một hành động nào đó không? Tôi đang cố gắng tìm ra cách tốt nhất để làm điều đó.

Có phương pháp hơi liên quan này trong UIApplication:

[UIApplication sharedApplication].idleTimerDisabled;

Sẽ thật tuyệt nếu bạn thay vào đó có một cái gì đó như thế này:

NSTimeInterval timeElapsed = [UIApplication sharedApplication].idleTimeElapsed;

Sau đó, tôi có thể thiết lập bộ hẹn giờ và kiểm tra định kỳ giá trị này và thực hiện một số hành động khi vượt quá ngưỡng.

Hy vọng rằng điều đó giải thích những gì tôi đang tìm kiếm. Có ai đã giải quyết vấn đề này chưa, hoặc có bất kỳ suy nghĩ nào về cách bạn sẽ làm nó? Cảm ơn.


Đâ là một câu hỏi tuyệt vời. Windows có khái niệm về một sự kiện OnIdle nhưng tôi nghĩ rằng ứng dụng này hiện không xử lý bất cứ điều gì trong bơm thông báo của nó so với thuộc tính idleTimerDisables của iOS mà dường như chỉ liên quan đến việc khóa thiết bị. Bất cứ ai cũng biết nếu có bất cứ điều gì thậm chí gần với khái niệm Windows trong iOS / MacOSX?
ném đá vào

Câu trả lời:


153

Đây là câu trả lời tôi đã tìm kiếm:

Có ứng dụng của bạn đại biểu lớp UIApplication. Trong tệp thực hiện, ghi đè phương thức sendEvent: như vậy:

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

trong đó maxIdleTime và idleTimer là các biến thể hiện.

Để làm việc này, bạn cũng cần sửa đổi main.m của mình để báo cho UIApplicationMain sử dụng lớp đại biểu của bạn (trong ví dụ này, AppDelegate) làm lớp chính:

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

3
Xin chào Mike, AppDelegate của tôi đang sử dụng từ NSObject Vì vậy, đã thay đổi UIApplication và triển khai các phương thức trên để phát hiện người dùng trở nên nhàn rỗi nhưng tôi đang gặp lỗi "Chấm dứt ứng dụng do ngoại lệ chưa được phát hiện 'NSI INTERNalInconsistencyException', lý do: 'Chỉ có thể có một UIA. ".. tôi còn cần phải làm gì nữa không ...?
Mihir Mehta

7
Tôi sẽ thêm rằng lớp con UIApplication nên tách biệt với lớp con UIApplicationDelegate
boliva

Tôi KHÔNG chắc chắn làm thế nào điều này sẽ hoạt động với thiết bị đi vào trạng thái không hoạt động khi bộ hẹn giờ ngừng bắn?
anonmys

không hoạt động đúng nếu tôi chỉ định sử dụng chức năng popToRootViewControll cho sự kiện hết thời gian. Nó xảy ra khi tôi hiển thị UIAlertView, sau đó là popToRootViewControll, sau đó tôi nhấn bất kỳ nút nào trên UIAlertView bằng một bộ chọn từ uiviewContoder đã được bật lên
Gargo

4
Rất đẹp! Tuy nhiên, cách tiếp cận này tạo ra rất nhiều NSTimertrường hợp nếu có nhiều chạm.
Andreas Ley

86

Tôi có một biến thể của giải pháp hẹn giờ nhàn rỗi không yêu cầu phân loại UIApplication. Nó hoạt động trên một lớp con UIViewContoder cụ thể, vì vậy rất hữu ích nếu bạn chỉ có một bộ điều khiển xem (như ứng dụng tương tác hoặc trò chơi có thể có) hoặc chỉ muốn xử lý thời gian chờ trong một bộ điều khiển xem cụ thể.

Nó cũng không tạo lại đối tượng NSTimer mỗi khi thiết lập lại bộ đếm thời gian nhàn rỗi. Nó chỉ tạo một cái mới nếu bộ đếm thời gian kích hoạt.

Mã của bạn có thể gọi resetIdleTimercho bất kỳ sự kiện nào khác có thể cần làm mất hiệu lực bộ đếm thời gian nhàn rỗi (chẳng hạn như đầu vào gia tốc đáng kể).

@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

(mã dọn dẹp bộ nhớ được loại trừ cho ngắn gọn.)


1
Rất tốt. Câu trả lời này đá! Đánh bại câu trả lời là đúng mặc dù tôi biết nó đã sớm hơn rất nhiều nhưng đây là một giải pháp tốt hơn bây giờ.
Chintan Patel

Điều này thật tuyệt, nhưng một vấn đề tôi đã tìm thấy: cuộn trong UITableViews không khiến nextResponder được gọi. Tôi cũng đã thử theo dõi qua touchesBegan: và touchesMoved:, nhưng không cải thiện. Có ý kiến ​​gì không?
Greg Maletic

3
@GregMaletic: tôi gặp vấn đề tương tự nhưng cuối cùng tôi đã thêm - (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView {NSLog (@ "Sẽ bắt đầu kéo"); } - (void) scrollViewDidScroll: (UIScrollView *) scrollView {NSLog (@ "Did Scroll"); [tự đặt lạiIdleTimer]; } bạn đã thử cái này chưa?
Akshay Aher

Cảm ơn. Điều này vẫn hữu ích. Tôi đã chuyển nó cho Swift và nó hoạt động rất tốt.
Mark.ewd

Bạn là một ngôi sao nhạc rock. kudos
Pras

21

Đối với nhanh chóng v 3.1

đừng quên bình luận dòng này trong 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)
   }
} 

tạo tệp main.swif và thêm tệp này (tên là quan trọng)

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

Quan sát thông báo trong bất kỳ lớp nào khác

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

2
Tôi không hiểu lý do tại sao chúng ta cần một tấm séc if idleTimer != niltrong sendEvent()phương pháp?
Guangyu Wang

Làm thế nào chúng ta có thể thiết lập giá trị timeoutInSecondstừ phản ứng dịch vụ web?
Người dùng_1191

12

Chủ đề này là một trợ giúp tuyệt vời và tôi đã gói nó vào một lớp con UIWindow để gửi thông báo. Tôi đã chọn thông báo để làm cho nó trở thành một khớp nối lỏng lẻo thực sự, nhưng bạn có thể thêm một đại biểu đủ dễ dàng.

Đây là ý chính:

http://gist.github.com/365998

Ngoài ra, lý do cho vấn đề phân lớp UIApplication là NIB được thiết lập để sau đó tạo 2 đối tượng UIApplication vì nó chứa ứng dụng và đại biểu. Lớp con UIWindow hoạt động tuyệt vời mặc dù.


1
bạn có thể cho tôi biết làm thế nào để sử dụng mã của bạn? tôi không hiểu làm thế nào để gọi nó
R. Dewi

2
Nó hoạt động rất tốt khi chạm, nhưng dường như không xử lý đầu vào từ bàn phím. Điều này có nghĩa là nó sẽ hết thời gian nếu người dùng đang gõ nội dung trên bàn phím gui.
Martin Wickman

2
Tôi cũng không thể hiểu cách sử dụng nó ... Tôi thêm người quan sát vào trình điều khiển chế độ xem của mình và hy vọng thông báo sẽ bị hủy khi ứng dụng không được chạm / không hoạt động .. nhưng không có gì xảy ra ... cộng với việc chúng ta có thể kiểm soát thời gian nhàn rỗi ở đâu? như tôi muốn thời gian nhàn rỗi là 120 giây để sau 120 giây IdleNotification sẽ kích hoạt, không phải trước đó.
Ans

5

Trên thực tế, ý tưởng phân lớp hoạt động tuyệt vời. Chỉ không làm cho đại biểu của bạn UIApplicationlớp con. Tạo một tệp khác kế thừa từ UIApplication(ví dụ myApp). Trong IB đặt lớp của fileOwnerđối tượng thành myAppvà trong myApp.m thực hiện sendEventphương thức như trên. Trong main.m làm:

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

et voilà!


1
Đúng, tạo một lớp con UIApplication độc lập dường như hoạt động tốt. Tôi để lại parm nil thứ hai trong chính.
Licks nóng

@Roby, hãy xem stackoverflow.com/questions/20088521/ của tôi .
Tirth

4

Tôi vừa gặp vấn đề này với một trò chơi được điều khiển bởi các chuyển động tức là đã tắt khóa màn hình nhưng sẽ kích hoạt lại khi ở chế độ menu. Thay vì bộ hẹn giờ, tôi gói gọn tất cả các cuộc gọi đến setIdleTimerDisabledtrong một lớp nhỏ cung cấp các phương thức sau:

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

disableIdleTimerhủy kích hoạt bộ đếm thời gian nhàn rỗi, enableIdleTimerDelayedkhi vào menu hoặc bất cứ thứ gì nên chạy với bộ đếm thời gian nhàn rỗi hoạt động và enableIdleTimerđược gọi từ applicationWillResignActivephương thức AppDelegate của bạn để đảm bảo tất cả các thay đổi của bạn được đặt lại đúng theo hành vi mặc định của hệ thống.
Tôi đã viết một bài viết và cung cấp mã cho lớp đơn xử lý IdleTimerManager Idle Timer Xử lý trong các trò chơi trên iPhone


4

Đây là một cách khác để phát hiện hoạt động:

Bộ hẹn giờ được thêm vào UITrackingRunLoopMode, vì vậy nó chỉ có thể kích hoạt nếu có UITrackinghoạt động. Nó cũng có một lợi thế tốt là không spam bạn cho tất cả các sự kiện chạm, do đó thông báo nếu có hoạt động trong những ACTIVITY_DETECT_TIMER_RESOLUTIONgiây cuối cùng . Tôi đặt tên cho bộ chọn keepAlivevì nó có vẻ là một trường hợp sử dụng thích hợp cho việc này. Tất nhiên bạn có thể làm bất cứ điều gì bạn muốn với thông tin có hoạt động gần đây.

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

làm sao vậy Tôi tin rằng rõ ràng bạn nên tự mình chọn "keepAlive" cho bất cứ nhu cầu nào bạn có. Có lẽ tôi đang thiếu quan điểm của bạn?
Mihai Timar

Bạn nói rằng đây là một cách khác để phát hiện hoạt động, tuy nhiên, điều này chỉ tạo ra một iVar là NSTimer. Tôi không thấy cách này trả lời câu hỏi của OP.
Jasper

1
Bộ hẹn giờ được thêm vào trong UITrackingRunLoopMode, vì vậy nó chỉ có thể kích hoạt nếu có hoạt động UITracking. Nó cũng có một ưu điểm tuyệt vời là không spam bạn cho tất cả các sự kiện chạm, do đó thông báo nếu có hoạt động trong giây ACTIVITY_DETECT_TIMER_RESOLNING cuối cùng. Tôi đặt tên cho bộ chọn keepAlive vì có vẻ như trường hợp sử dụng thích hợp cho việc này. Tất nhiên bạn có thể làm bất cứ điều gì bạn muốn với thông tin có hoạt động gần đây.
Mihai Timar

1
Tôi muốn cải thiện câu trả lời này. Nếu bạn có thể giúp với các gợi ý về việc làm cho nó rõ ràng hơn, nó sẽ là một trợ giúp tuyệt vời.
Mihai Timar

Tôi đã thêm lời giải thích của bạn vào câu trả lời của bạn. Nó có ý nghĩa hơn rất nhiều bây giờ.
Jasper

3

Cuối cùng, bạn cần xác định những gì bạn cho là không hoạt động - có phải là kết quả của việc người dùng không chạm vào màn hình hay đó là trạng thái của hệ thống nếu không sử dụng tài nguyên máy tính? Có thể, trong nhiều ứng dụng, người dùng sẽ làm gì đó ngay cả khi không chủ động tương tác với thiết bị thông qua màn hình cảm ứng. Mặc dù người dùng có thể quen với khái niệm thiết bị sẽ đi ngủ và thông báo rằng nó sẽ xảy ra thông qua việc làm mờ màn hình, nhưng không nhất thiết là họ sẽ mong đợi điều gì đó xảy ra nếu họ không sử dụng - bạn cần cẩn thận về những gì bạn sẽ làm. Nhưng quay trở lại với tuyên bố ban đầu - nếu bạn coi trường hợp đầu tiên là định nghĩa của bạn, không có cách nào thực sự dễ dàng để làm điều này. Bạn cần nhận từng sự kiện chạm, chuyển nó theo chuỗi phản hồi khi cần thiết trong khi lưu ý thời gian nhận được. Điều đó sẽ cung cấp cho bạn một số cơ sở để thực hiện tính toán nhàn rỗi. Nếu bạn coi trường hợp thứ hai là định nghĩa của mình, bạn có thể chơi với thông báo NSPostWhenIdle để thử và thực hiện logic của mình tại thời điểm đó.


1
Chỉ cần làm rõ, tôi đang nói về tương tác với màn hình. Tôi sẽ cập nhật câu hỏi để phản ánh điều đó.
Mike McMaster

1
Sau đó, bạn có thể thực hiện một cái gì đó bất cứ khi nào chạm vào bạn cập nhật một giá trị mà bạn kiểm tra hoặc thậm chí đặt (và đặt lại) một bộ đếm thời gian nhàn rỗi để kích hoạt, nhưng bạn cần phải tự thực hiện nó, bởi vì như đã nói, điều gì tạo nên sự nhàn rỗi khác nhau giữa ứng dụng khác nhau.
Louis Gerbarg

1
Tôi đang xác định "nhàn rỗi" đúng như thời gian kể từ lần cuối chạm vào màn hình. Tôi hiểu rằng tôi sẽ cần phải tự thực hiện nó, tôi chỉ tự hỏi cách "tốt nhất" sẽ là gì, nói, chặn màn hình chạm, hoặc nếu ai đó biết về một phương pháp thay thế để xác định điều này.
Mike McMaster

3

Có một cách để làm ứng dụng này rộng mà không cần bộ điều khiển riêng lẻ phải làm bất cứ điều gì. Chỉ cần thêm một nhận dạng cử chỉ mà không hủy chạm. Bằng cách này, tất cả các lần chạm sẽ được theo dõi cho bộ đếm thời gian và các lần chạm và cử chỉ khác hoàn toàn không bị ảnh hưởng nên không ai phải biết về điều đó.

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

Trong phương thức khởi chạy của đại biểu ứng dụng của bạn, chỉ cần gọi addGesture và bạn đã hoàn tất. Tất cả các lần chạm sẽ đi qua các phương thức của CatchAllGesture mà không ngăn cản chức năng của người khác.


1
Tôi thích cách tiếp cận này, đã sử dụng nó cho một vấn đề tương tự với Xamarin: stackoverflow.com/a/51727021/250164
Wolfgang Schreurs

1
Hoạt động rất tốt, cũng có vẻ như kỹ thuật này được sử dụng để kiểm soát mức độ hiển thị của các điều khiển UI trong AVPlayerViewControll (tham chiếu API riêng ). Ứng dụng ghi đè -sendEvent:là quá mức cần thiết và UITrackingRunLoopModekhông xử lý nhiều trường hợp.
La Mã B.

@RomanB. Vâng chính xác. Khi bạn đã làm việc với iOS đủ lâu, bạn sẽ luôn biết sử dụng "đúng cách" và đây là cách đơn giản để thực hiện một cử chỉ tùy chỉnh như nhà phát triển
định.apple.com / document / uikit / uigesturerecognizer / khăn lau

Điều gì làm cho trạng thái thành .fails hoàn thành trong touchesEnded?
ném đá

Tôi thích cách tiếp cận này nhưng khi thử nó dường như chỉ bắt được vòi chứ không phải bất kỳ cử chỉ nào khác như pan, vuốt, v.v. trong các lần chạm. Đó có phải là dự định?
ném đá
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.