6

I'm trying to implement a lock screen in the SwiftUI app.

I need to track every event in order to restart the lock timer.

In UIKit app, I used this approach - overriding UIApplication, which allows being aware of any event across the app:

override func sendEvent(_ event: UIEvent) {
  super.sendEvent(event)

  switch event.type {
  case .touches:
    // Post Notification or Delegate here
  default:
    break
  }
}

But in SwiftUI it is not supported anymore. I tried to add

.onTapGesture {}

to the root ContentView, but it doesn't work as expected.

Is there any way to avoid adding

.onTapGesture {}

to every single view in the app?

  • You can see this answer: https://stackoverflow.com/a/60010955/8697793 - instead of endEditing you can call your custom function. – pawello2222 Sep 16 '20 at 20:42
  • 1
    Yes, this is a great answer, but there are some issues with iOS 14 without the Scenedelegate and Window – Owen Rivera Sep 16 '20 at 21:03

2 Answers2

7

Here is a possible solution:

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
        }
    }
}

extension UIApplication {
    func addTapGestureRecognizer() {
        guard let window = windows.first else { return }
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction))
        tapGesture.requiresExclusiveTouchType = false
        tapGesture.cancelsTouchesInView = false
        tapGesture.delegate = self
        window.addGestureRecognizer(tapGesture)
    }

    @objc func tapAction(_ sender: UITapGestureRecognizer) {
        print("tapped")
    }
}

extension UIApplication: UIGestureRecognizerDelegate {
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true // set to `false` if you don't want to detect tap during other gestures
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • I have this error when using this code `'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead` – Roland Lariotte Jun 14 '21 at 20:08
  • @RolandLariotte should be a warning not an error. You can get the equivalent of `windows.first` with this. `connectedScenes.compactMap { $0 as? UIWindowScene }.flatMap { $0.windows }.first { $0.isKeyWindow }` Other answers you'll see on SO will include a check for `.activationState == foregroundActive` but you should leave that off for this particular implementation. – Nathan Dudley Mar 10 '22 at 05:06
  • Solution for 'windows' was deprecated in iOS 15.0. let scenes = UIApplication.shared.connectedScenes let windowScene = scenes.first as? UIWindowScene guard let window = windowScene?.windows.first else { return } – Rahul Gupta Nov 01 '22 at 09:28
2

Based on pawello2222's solution, I added UIPanGestureRecognizer to be able to recognize when scrolling or swiping around the app too. Also added a notification observer to recognize when typing text from keyboard. The solution would look like this:

@main
struct YourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
                    // Interaction recognizer implementation
                    UIApplication.shared.addInteractionRecognizer()
                }
        }
    }
}

extension UIApplication {
    func addInteractionRecognizer() {
        // Notification observer to track text changes from keyboard
        NotificationCenter.default.addObserver(self, selector: #selector(didInteractWithApp), name: UITextField.textDidChangeNotification, object: nil)
        
        guard let window = windows.first else { return }
        
        // Gestures recognizers to track
        let gestureRecognizers = [
            UITapGestureRecognizer(target: self, action: #selector(didInteractWithApp)),
            UIPanGestureRecognizer(target: self, action: #selector(didInteractWithApp))
        ]
        
        gestureRecognizers.forEach {
            $0.requiresExclusiveTouchType = false
            $0.cancelsTouchesInView = false
            $0.delegate = self
            window.addGestureRecognizer($0)
        }
    }
    
    @objc func didInteractWithKeyboard() {
        // Restart the lock timer
    }
    
    @objc func didInteractWithApp(_ sender: UIGestureRecognizer) {
        // Optional: Validate UIPanGestureRecognizer has ended, cancelled or failed, to prevent overloading for restarting timer. Remove if not needed
        let allowedStates: [UIGestureRecognizer.State] = [.ended, .cancelled, .failed]
        if sender as? UIPanGestureRecognizer != nil, !allowedStates.contains(sender.state) {
            return
        }
        
        // Restart the lock timer
    }
}

extension UIApplication: UIGestureRecognizerDelegate {
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        // Set to true to recognize gestures specified above while allowing user interact with other gestures in the app and not to block them with them
        return true
    }
}
jguillen
  • 180
  • 1
  • 9
  • How to set up a timer as we cant add properties in UIApplication extension? – Usama bin Attique Feb 28 '23 at 14:40
  • Keyboard taps doesn't seem to be registered for TextEditor views :) Otherwise it's a nice solution. Thanks! – Nicolai Harbo May 17 '23 at 11:23
  • Adding NotificationCenter.default.addObserver(self, selector: #selector(didInteractWithApp), name: UITextView.textDidChangeNotification, object: nil) will make it work for TextEditor as well :) – Nicolai Harbo May 17 '23 at 12:01