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


IMMEDIATE GOAL: throughout a trackpad click on + drag gesture, detect and react to an extra (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 part’s title is grabbed, a pan gesture from an extra two fingers will trigger the background (somewhat than the part) 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


  • taking a look at touches and areas of the assorted gesture recognizers
  • taking a look at 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 your entire view and in addition listens for trackpad gestures (similar outcomes as above)


import SwiftUI

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

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

struct TrackpadUIKitGesture: UIViewControllerRepresentable {

    let title: 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(title: title)

class TrackpadDelegate: NSObject, UIGestureRecognizerDelegate {

    let title: String

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

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

    @objc func trackpadPanInView(_ gestureRecognizer: UIPanGestureRecognizer) {
        guard let view = gestureRecognizer.view else {
            fatalError("(title) 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("(title): touches: (touches)")
        log("(title): state: (state)")
        log("(title): translation: (translation)")
        log("(title): location: (location)")

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


Leave a Reply