Arman
Arman

Reputation: 1318

Detect and forward taps anywhere on screen in SwiftUI

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?

Upvotes: 11

Views: 4744

Answers (2)

pawello2222
pawello2222

Reputation: 54611

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.

Upvotes: 13

Chris
Chris

Reputation: 8126

did you try to set

 cancelsTouchesInView = false 

in your TapGesture?

Upvotes: 0

Related Questions