11

In a SwiftUI app, I need to detect any tap on the screen. Only detect, and then forward it to underlying views. As a use case, think of sending "online" user status updates to the server in response to any user activity.

I certainly don't want to add gesture recognizers to every view for this purpose. I tried adding a global one in SceneDelegate. This works as far as detecting any tap goes:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {        
    // ... snip other code
    initGlobalTapRecognizer()
}

// MARK: UIGestureRecognizerDelegate
extension SceneDelegate: UIGestureRecognizerDelegate {
    func initGlobalTapRecognizer() {
        let tapGesture = UITapGestureRecognizer(target: self, action: nil)
        tapGesture.delegate = self
        window?.addGestureRecognizer(tapGesture)
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        print("tapped")
        return true
    }
}

But this breaks some SwiftUI controls. For example, buttons work, but TabView no longer responds to taps.

I tried another way, using simultaneousGesture as suggest here:

struct ContentView: View {
    @State var selectedTab: Int = 1

    var body: some View {
        TabView(selection: $selectedTab) {
            VStack {
                Text("tab 1")
                Button(action: { print("button 1 click") }, label: { Text("button 1") })
            }
            .tabItem( { Text("tab 1") } )
            .tag(1)

            VStack {
                Text("tab 2")
            }
            .tabItem( { Text("tab 2") } )
            .tag(2)
        }
        .contentShape(Rectangle())
        .simultaneousGesture(TapGesture().onEnded({ print("simultaneous gesture tap") }))
    }
}

Same result, buttons work, but TabView is broken.

Any ideas how to get this working?

1
  • If you want to detect any user activity at all, consider adapting my solution here. This was for implementing an idle timer, but can just as easily be adapted for the use case here. Commented Jan 2, 2024 at 18:20

2 Answers 2

13
+50

As a use case, think of sending "online" user status updates to the server in response to any user activity.

  1. Create an extension for a global gesture recogniser:
extension UIApplication {
    func addGestureRecognizer() {
        guard let window = windows.first else { return }
        let gesture = UITapGestureRecognizer(target: window, action: nil)
        gesture.requiresExclusiveTouchType = false
        gesture.cancelsTouchesInView = false
        gesture.delegate = self
        window.addGestureRecognizer(gesture)
    }
}
extension UIApplication: UIGestureRecognizerDelegate {
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        print("touch detected")
        return true
    }

    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}
  1. Add it to the root window:

SwiftUI 1 version:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {        
    // ...
    addGestureRecognizer()
}

SwiftUI 2 version:

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear(perform: UIApplication.shared.addGestureRecognizer)
        }
    }
}
  1. Now the gesture recogniser is attached to the root window and will be recognised simultaneously with other controls:
struct ContentView: View {
    var body: some View {
        TabView {
            Button("Tap me") {
                print("Button tapped")
            }
            .tabItem {
                Text("One")
            }
            List {
                Text("Hello")
                Text("World")
            }
            .tabItem {
                Text("Two")
            }
        }
    }
}

Alternatively, you can create a custom AnyGestureRecongizer as proposed here to detect any gestures.

Sign up to request clarification or add additional context in comments.

3 Comments

Awesome, thanks a bunch. Was having trouble with this specifically for certain SwiftUI controls like described in the question.
Great work! It saved my time.
In Xcode 16, "extension UIApplication: UIGestureRecognizerDelegate" generates a warning "Extension declares a conformance of imported type 'UIApplication' to imported protocol 'UIGestureRecognizerDelegate'; this will not behave correctly if the owners of 'UIKit' introduce this conformance in the future. Add '@retroactive' to silence this warning." Would it be safe to simply update to "extension UIApplication: @retroactive UIGestureRecognizerDelegate { ... }"?
0

did you try to set

 cancelsTouchesInView = false 

in your TapGesture?

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.