ios – UIKit trackpad: whereas click on + drag in progress, detect extra touch-less drag


IMMEDIATE GOAL: throughout a trackpad click on + drag gesture, detect and react to a further (no-click) trackpad drag / pan gesture.

PROBLEM: UIKit appears to disregard the extra, no-click two-finger pan on trackpad as long as the primary trackpad click on + drag is ongoing.

BIG PICTURE UX: a “click on + drag” and a “click on + drag + one other pan” ought to have completely different habits.

EXAMPLE: the specified habits is much like Figma: whereas a element’s identify is grabbed, a pan gesture from a further two fingers will trigger the background (slightly than the element) to maneuver round.


  • simply click on + drag on trackpad: a gesture with touches = 1 fires
  • simply pan / drag (no click on) on trackpad: a gesture with touches = 0 fires
  • throughout a click on + drag, including one other pan/drag: the identical gesture as from the touches = 1 fires


  • touches and places of the varied gesture recognizers
  • UIGestureRecognizerDelegate’s delegate strategies like gestureRecognizer(_:shouldReceive:)
  • minTouches = 1 on one trackpad UIKit gesture recognizer (i.e. the press+drag) and maxTouches = 0 for the opposite (i.e. the no-click drag)
  • (code not shared right here): an injectable UIKit wrapper that sits over the complete view and likewise listens for trackpad gestures (identical outcomes as above)


import SwiftUI

struct StackOverflowUIKitView: View {
    var nodeView: some View {
            .body(width: 100, peak: 100)
            .overlay(TrackpadUIKitGesture(identify: "Node"))
            .place(x: 500, y: 400)

    var physique: some View {
        ZStack { nodeView }
            .border(.purple, width: 18)
            .background(TrackpadUIKitGesture(identify: "Graph"))

struct TrackpadUIKitGesture: UIViewControllerRepresentable {

    let identify: String

    func makeUIViewController(context: Context) -> UIViewController {
        let vc = UIViewController()
        let delegate = context.coordinator

        let trackpadPan = UIPanGestureRecognizer(
            goal: delegate,
            motion: #selector(delegate.trackpadPanInView))
        trackpadPan.allowedScrollTypesMask = [.continuous, .discrete]
        trackpadPan.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirectPointer.rawValue)]
        trackpadPan.delegate = delegate

        return vc

    func updateUIViewController(_ uiViewController: UIViewController,
                                context: Context) { }

    func makeCoordinator() -> TrackpadDelegate {
        TrackpadDelegate(identify: identify)

class TrackpadDelegate: NSObject, UIGestureRecognizerDelegate {

    let identify: String

    init(identify: String) {
        self.identify = identify

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                           shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {

    @objc func trackpadPanInView(_ gestureRecognizer: UIPanGestureRecognizer) {
        guard let view = gestureRecognizer.view else {
            fatalError("(identify) error: no view.")

        let touches = gestureRecognizer.numberOfTouches
        let state = gestureRecognizer.state

        let translation = gestureRecognizer.translation(in: view).toCGSize
        let location = gestureRecognizer.location(in: view)

        log("(identify): touches: (touches)")
        log("(identify): state: (state)")
        log("(identify): translation: (translation)")
        log("(identify): location: (location)")

        if touches > 1 {
            let touch0 = gestureRecognizer.location(ofTouch: 0, in: view)
            log("(identify): touch0: (touch0)")


Leave a Reply