5

I'm trying to create a timeout function for an app I'm develop using Swift 2 but in swift 2, you can put this code in the app delegate and it works but it does not detect any keyboard presses, button presses, textfield presses, and etc:

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    super.touchesBegan(touches, withEvent: event);
    let allTouches = event!.allTouches();

    if(allTouches?.count > 0) {
        let phase = (allTouches!.first as UITouch!).phase;
        if(phase == UITouchPhase.Began || phase == UITouchPhase.Ended) {
            //Stuff
            timeoutModel.actionPerformed();
        }
    }
}

Before swift 2, I was able to have the AppDelegate subclass UIApplication and override sendEvent: like this:

-(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)
            [[InactivityModel instance] actionPerformed];
    }
}

The code above works for every touch but the swift equivalent only works when a view does not exist above that UIWindow's hierarchy?

Does anyone know a way to detect every touch in the application?

FireDragonMule
  • 1,347
  • 2
  • 16
  • 27
  • It has something to do with iOS 9 instead of Swift 2. I've got similar solution in my custom `UIWindow` and it stops working as well. Probably because of new iOS 9 split app view, etc. So they redone it to support these new features - two open apps, keyboard, ... Not 100% sure, just thinking aloud ... – zrzka Jul 28 '15 at 09:46

3 Answers3

19

As I have something similar in my application, I just tried to fix it:

  • override sendEvent in UIWindow - doesn't work
  • override sendEvent in delegate - doesn't work

So the only way is to provide custom UIApplication subclass. My code so far (works on iOS 9) is:

@objc(MyApplication) class MyApplication: UIApplication {

  override func sendEvent(event: UIEvent) {
    //
    // Ignore .Motion and .RemoteControl event
    // simply everything else then .Touches
    //
    if event.type != .Touches {
      super.sendEvent(event)
      return
    }

    //
    // .Touches only
    //
    var restartTimer = true

    if let touches = event.allTouches() {
      //
      // At least one touch in progress?
      // Do not restart auto lock timer, just invalidate it
      //
      for touch in touches.enumerate() {
        if touch.element.phase != .Cancelled && touch.element.phase != .Ended {
          restartTimer = false
          break
        }
      }
    }

    if restartTimer {
      // Touches ended || cancelled, restart auto lock timer
      print("Restart auto lock timer")
    } else {
      // Touch in progress - !ended, !cancelled, just invalidate it
      print("Invalidate auto lock timer")
    }

    super.sendEvent(event)
  }

}

Why there's @objc(MyApplication). That's because Swift mangles names in a different way then Objective-C and it just says - my class name in Objective-C is MyApplication.

To make it working, open your info.plist and add row with Principal class key and MyApplication value (MyApplication is what's inside @objc(...), not your Swift class name). Raw key is NSPrincipalClass.

enter image description here

zrzka
  • 20,249
  • 5
  • 47
  • 73
4

UIWindow also has a sendEvent method that you can override. That would allow you to track the time since the last screen touch. Swift 4:

class IdlingWindow: UIWindow {
    /// Tracks the last time this window was interacted with
    var lastInteraction = Date.distantPast

    override func sendEvent(_ event: UIEvent) {
        super.sendEvent(event)
        lastInteraction = Date()
    }
}

If you're using a storyboard, you can load it in didFinishLaunchingWithOptions:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: IdlingWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        window = IdlingWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = UIStoryboard.init(name: "Main", bundle: nil).instantiateInitialViewController()
        window?.makeKeyAndVisible()
        return true
    }
    :
}

extension UIApplication {
    /// Conveniently gets the last interaction time
    var lastInteraction: Date {
        return (keyWindow as? IdlingWindow)?.lastInteraction ?? .distantPast
    }
}

Now elsewhere in your app, you can check for inactivity like this:

if UIApplication.shared.lastInteraction.timeIntervalSinceNow < -2 {
    // the window has been idle over 2 seconds
}
markiv
  • 1,578
  • 16
  • 12
  • Nice solution, but unfortunately: "'keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes" – wider-spider Sep 13 '19 at 12:54
0

@markiv answer works like a charm, with two issues:

  1. keyWindow
    • "'keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes"

Can be solved like this:

let kw = UIApplication.shared.windows.filter {$0.isKeyWindow}.first found here

see answer of @matt

  1. AppDelegate - I get this message:

    • The app delegate must implement the window property if it wants to use a main storyboard file

One can subclass UIWindow - found the answer here. Combine this with the IdlingWindow class.

Message is gone.

var customWindow: IdlingWindow?    
var window: UIWindow? {
    get {
        customWindow = customWindow ?? IdlingWindow(frame: UIScreen.mainScreen().bounds)
        return customWindow
    }
    set { }
}
wider-spider
  • 465
  • 4
  • 12