🚀

Multi-Finger Taps in SwiftUI: Implementing 2-Finger & 3-Finger Gestures

(Updated on )

TL;DR: The native SwiftUI TapGesture only supports configuring the number of taps, not the number of touches (fingers). To implement two-finger or three-finger taps, we must leverage the power of UIKit.

Prior to iOS 18, wrapping a UIView via UIViewRepresentable was the only solution. However, in iOS 18+ (including the current iOS 26 context), Apple introduced the more efficient UIGestureRecognizerRepresentable protocol, which allows us to bridge gesture recognizers directly.

Starting with iOS 18, you can use UIGestureRecognizerRepresentable to expose UIKit’s UITapGestureRecognizer directly to SwiftUI. This approach is more performant than creating a full intermediate UIView and feels much more native to Swift coding styles.

Code Example

Swift
import SwiftUI

struct TwoFingerTapDemo: View {
    var body: some View {
        Rectangle()
            .foregroundStyle(.orange)
            .frame(width: 200, height: 200)
            .overlay(Text("Tap with 2 Fingers"))
            .onTapGesture {
                print("Single Tap (Native)")
            }
            // Apply custom two-finger gesture
            .gesture(TwoFingerTapGesture {
                print("Two-finger tap detected!")
            })
    }
}

// Define the bridgeable gesture
struct TwoFingerTapGesture: UIGestureRecognizerRepresentable {
    let action: () -> Void

    func makeUIGestureRecognizer(context: Context) -> UITapGestureRecognizer {
        let gesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleGesture))
        gesture.numberOfTouchesRequired = 2 // Key: Require 2 fingers
        gesture.delegate = context.coordinator
        return gesture
    }

    func updateUIGestureRecognizer(_ recognizer: UITapGestureRecognizer, context: Context) {}

    func makeCoordinator(converter: CoordinateSpaceConverter) -> Coordinator {
        Coordinator(action: action)
    }

    class Coordinator: NSObject, UIGestureRecognizerDelegate {
        let action: () -> Void

        init(action: @escaping () -> Void) {
            self.action = action
        }

        @objc func handleGesture() {
            action()
        }

        // Allow coexistence with other gestures (e.g., native single tap)
        func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer) -> Bool {
            true 
        }
    }
}

Advantages

  • Lightweight: No need to create an intermediate UIView layer.
  • State Synchronization: Easier to integrate with SwiftUI’s @State or Transaction.

Solution 2: Legacy Support (iOS 17 & Below)

For versions that do not support the new protocol, we need to create a transparent UIView via UIViewRepresentable and attach the gesture recognizer to it.

Implementation Steps

  1. Create a transparent UIView subclass and configure a UITapGestureRecognizer.
  2. Wrap this view using UIViewRepresentable.
  3. Overlay it on the target SwiftUI view using .overlay().

Code Example

Swift
struct LegacyTwoFingerTapDemo: View {
    var body: some View {
        Rectangle()
            .foregroundStyle(.blue)
            .frame(width: 200, height: 200)
            .onTwoFingerTap {
                print("Two-finger tap (Legacy Mode)")
            }
    }
}

// Convenience Modifier
extension View {
    func onTwoFingerTap(perform action: @escaping () -> Void) -> some View {
        overlay(TwoFingerTapLayer(action: action))
    }
}

struct TwoFingerTapLayer: UIViewRepresentable {
    let action: () -> Void

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        view.backgroundColor = .clear // Ensure transparency
        
        let gesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.onTap))
        gesture.numberOfTouchesRequired = 2
        view.addGestureRecognizer(gesture)
        
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(action: action)
    }
    
    class Coordinator: NSObject {
        let action: () -> Void
        init(action: @escaping () -> Void) { self.action = action }
        
        @objc func onTap() { action() }
    }
}

Important Considerations

  • Gesture Conflicts: If you use both the native .onTapGesture and a custom multi-finger gesture, be mindful of the SwiftUI view hierarchy order. It is generally recommended to place .onTapGesture (single tap) after the .onTwoFingerTap modifier, or implement delegate methods within the UIViewRepresentable coordinator to explicitly handle simultaneous recognition.

Further Reading

Related Tips

Subscribe to Fatbobman

Weekly Swift & SwiftUI highlights. Join developers.

Subscribe Now