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.
Solution 1: iOS 18+ (Recommended)
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
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
UIViewlayer. - State Synchronization: Easier to integrate with SwiftUI’s
@StateorTransaction.
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
- Create a transparent
UIViewsubclass and configure aUITapGestureRecognizer. - Wrap this view using
UIViewRepresentable. - Overlay it on the target SwiftUI view using
.overlay().
Code Example
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
.onTapGestureand a custom multi-finger gesture, be mindful of the SwiftUI view hierarchy order. It is generally recommended to place.onTapGesture(single tap) after the.onTwoFingerTapmodifier, or implement delegate methods within theUIViewRepresentablecoordinator to explicitly handle simultaneous recognition.