1

I want to

  1. Detect global touch up event anywhere in the screen
  2. Will not affect components like buttons, custom views underneath. The underneath buttons, custom views are still able to receive the tap event.

What I did is

  1. Install UITapGestureRecognizer in UIApplication.shared.keyWindow
  2. In UIGestureRecognizerDelegate, returns true for shouldRecognizeSimultaneouslyWith, so that the top level keyWindow will not block buttons, custom views from receiving the tap event.

This is my code

import UIKit

extension UIWindow {
    static var key: UIWindow! {
        if #available(iOS 13, *) {
            return UIApplication.shared.windows.first { $0.isKeyWindow }
        } else {
            return UIApplication.shared.keyWindow
        }
    }
}

extension ViewController: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
           shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer)
            -> Bool {
        print(">>> shouldRecognizeSimultaneouslyWith returns true")
        return true
    }
}

class ViewController: UIViewController {
    // Lazy is required as self is not ready yet without lazy.
    private lazy var globalGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(globalTapped))
    
    private func installGlobalGestureRecognizer() {
        UIWindow.key.removeGestureRecognizer(globalGestureRecognizer)
        UIWindow.key.addGestureRecognizer(globalGestureRecognizer)
        
        globalGestureRecognizer.delegate = self
    }

    @objc func globalTapped() {
        print(">>> global tapped")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func buttonClicked(_ sender: Any) {
        print("yellow button tap\n")
    }
    
    @IBAction func handleTap(_ gesture: UITapGestureRecognizer) {
        print("red view tap\n")
    }
    
    @IBAction func installButtonClicked(_ sender: Any) {
        print("install global gesture")
        installGlobalGestureRecognizer()
    }
}

This is what happens when the red custom view being tapped

When red custom view being tapped (Work as expected)

install global gesture
>>> shouldRecognizeSimultaneouslyWith returns true
>>> shouldRecognizeSimultaneouslyWith returns true
>>> global tapped
red view tap

When the yellow button is being tapped (Global gesture not working)

install global gesture
yellow button tap

This is how I install the tap event handler, for both custom red view and yellow button.

enter image description here

enter image description here

Does anyone have any idea, why shouldRecognizeSimultaneouslyWith is not called, when the button is tapped? I expect when tapping on the yellow button

  • shouldRecognizeSimultaneouslyWith executed and returns true
  • yellow button tap event handler executed
  • global gesture tap event handler executed

Thanks.

Kiron Paul
  • 187
  • 2
  • 7
Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • 1
    Part of the issue is that `.touchUpInside` on a `UIControl` is not a **Tap** gesture. You may need another approach. Here's an (older) article that may be worth a read: https://dzone.com/articles/event-dispatching-in-swift-with-protocol-extension ... and you might want to look into Receiving and Handling Events with [Combine](https://developer.apple.com/documentation/combine/receiving-and-handling-events-with-combine) – DonMag Jul 30 '21 at 16:25
  • I'd posted a simple solution for your global touch up that works in all cases, in [your previous question](https://stackoverflow.com/questions/68566036/how-to-implement-a-global-gesture-recognizer-which-detects-tap-up-event?rq=1). Did it not suit your requirements? Or did you just not see it – Peter Parker Aug 06 '21 at 02:36

2 Answers2

3

as @DonMag correctly said, this happens because button touches are not handled with UIGestureRecognizers

UIControl gets touches directly from UIWindow, and it, in turn, gets it from UIApplication. You can subclass any of it to handle touches.

  1. To use UIWindow subclass you need to create its instance in SceneDelegate, and then manually reinitialise your storyboard, like this:
class CustomWindow: UIWindow {
    override func sendEvent(_ event: UIEvent) {
        super.sendEvent(event)
        print(#function, event)
    }
}

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }
        window = CustomWindow(windowScene: scene)
        window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
    }
}
  1. I prefer using UIApplication subclass. It takes a little bit more changes but doesn't needs that reinitialisation logic. Also if you use many scenes for iPad, you don't need to think about it too:
  • subclass UIApplication
class CustomUIApplication: UIApplication {
    override func sendEvent(_ event: UIEvent) {
        super.sendEvent(event)
        print(#function, event)
    }
}
  • remove @main annotation from AppDelegate
  • create main.swift with following content:
UIApplicationMain(
    CommandLine.argc,
    CommandLine.unsafeArgv,

    NSStringFromClass(CustomUIApplication.self),
    NSStringFromClass(AppDelegate.self)
)
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
0

Place a UIView as the background view and then place the UIButton as a different layer above the background view layer (important: should confirm that the UIButton is not placed inside the new background view) and give the gesture recogniser to the background view.

Wide Angle Technology
  • 1,184
  • 1
  • 8
  • 28