UIKitGesturesForSwiftUI
Advanced gesture recognizers for SwiftUI, leveraging UIGestureRecognizerRepresentable for full UIKit feature parity and complex interaction support.
Install / Use
/learn @jacobvanorder/UIKitGesturesForSwiftUIREADME
UIKitGesturesForSwiftUI
Advanced multi-touch gesture recognizers for SwiftUI, bringing the full power of UIKit's UIGestureRecognizer to SwiftUI with complete feature parity and Swift 6 concurrency support.
Written by @jacobvo.
A.I. Disclaimer:
Initial concept of one gesture hand-coded but then Claude assisted with replication of additional gestures.
Why This Library?
SwiftUI's built-in gesture system is powerful but has limitations:
DragGestureonly supports single-finger dragging- No native support for multi-finger gestures (2+ fingers)
- Limited access to UIKit-specific features like velocity, number of touches, and precise gesture states
- No control over gesture recognizer delegate methods for complex gesture interactions
UIKitGesturesForSwiftUI bridges this gap by exposing UIKit's full gesture recognizer capabilities directly in SwiftUI.
Features
✅ Multi-finger gesture support - Pan, tap, swipe, pinch, rotate with 2+ fingers
✅ Full UIKit feature parity - Access velocity, translation, rotation, scale, and more
✅ Gesture delegate control - Customize simultaneous recognition, failure requirements, and touch handling
✅ Swift 6 concurrency safe - All closures are @MainActor isolated
✅ Declarative builder API - Chain .onBegan, .onChanged, .onEnded naturally
✅ Comprehensive documentation - Inline docs for every gesture and method
✅ Somewhat tested - 23 unit tests covering core functionality
Requirements
- iOS 18.0+
- Swift 6.0+
- Xcode 16.0+
Installation
Swift Package Manager
Add this package to your Package.swift:
dependencies: [
.package(url: "https://github.com/yourusername/UIKitGesturesForSwiftUI.git", from: "1.0.0")
]
Or in Xcode:
- File → Add Package Dependencies
- Enter the repository URL
- Select version and add to your target
Quick Start
import SwiftUI
import UIKitGesturesForSwiftUI
struct ContentView: View {
@State private var offset = CGSize.zero
var body: some View {
Rectangle()
.fill(.blue)
.frame(width: 200, height: 200)
.offset(offset)
.gesture(
MultiFingerPanGesture(
minimumNumberOfTouches: 2,
maximumNumberOfTouches: 2
)
.onChanged { recognizer in
let translation = recognizer.translation(in: recognizer.view)
offset = CGSize(width: translation.x, height: translation.y)
}
)
}
}
Available Gestures
MultiFingerPanGesture
Continuous gesture for tracking multi-finger panning with velocity and translation.
MultiFingerPanGesture(
minimumNumberOfTouches: 2,
maximumNumberOfTouches: 2
)
.onBegan { recognizer in
print("Pan started")
}
.onChanged { recognizer in
let translation = recognizer.translation(in: recognizer.view)
let velocity = recognizer.velocity(in: recognizer.view)
print("Translation: \(translation), Velocity: \(velocity)")
}
.onEnded { recognizer in
let finalVelocity = recognizer.velocity(in: recognizer.view)
print("Pan ended with velocity: \(finalVelocity)")
}
Use Cases:
- Two-finger scrolling
- Multi-touch drag operations
- Gesture-based navigation
MultiFingerTapGesture
Discrete gesture for detecting multi-finger taps with configurable tap count.
MultiFingerTapGesture(
numberOfTouchesRequired: 3,
numberOfTapsRequired: 2 // Double-tap with 3 fingers
)
.onEnded { recognizer in
print("Triple-finger double-tap detected!")
}
Use Cases:
- Accessibility shortcuts
- Hidden debug menus
- Advanced user interactions
MultiFingerPinchGesture
Continuous gesture for tracking pinch-to-zoom with scale and velocity.
@State private var scale: CGFloat = 1.0
MultiFingerPinchGesture()
.onChanged { recognizer in
scale *= recognizer.scale
recognizer.scale = 1.0 // Reset for next update
}
.onEnded { recognizer in
let finalVelocity = recognizer.velocity
print("Pinch ended with velocity: \(finalVelocity)")
}
Use Cases:
- Zoom controls
- Image scaling
- Map interactions
MultiFingerRotationGesture
Continuous gesture for tracking rotation with angle and velocity.
@State private var rotation: Angle = .zero
MultiFingerRotationGesture()
.onChanged { recognizer in
rotation += Angle(radians: recognizer.rotation)
recognizer.rotation = 0 // Reset for next update
}
.onEnded { recognizer in
let finalVelocity = recognizer.velocity
print("Rotation velocity: \(finalVelocity) radians/sec")
}
Use Cases:
- Image rotation
- 3D object manipulation
- Creative tools
MultiFingerSwipeGesture
Discrete gesture for detecting directional swipes with multiple fingers.
MultiFingerSwipeGesture(
numberOfTouchesRequired: 3,
direction: .down
)
.onEnded { recognizer in
print("Three-finger swipe down detected!")
}
Directions: .up, .down, .left, .right
Use Cases:
- Navigation gestures
- App switching
- Custom gesture controls
MultiFingerLongPressGesture
Continuous gesture for long-press detection with configurable duration and movement tolerance.
MultiFingerLongPressGesture(
numberOfTouchesRequired: 2,
minimumPressDuration: 1.0,
allowableMovement: 10
)
.onBegan { recognizer in
print("Long press began")
}
.onEnded { recognizer in
print("Long press ended")
}
Use Cases:
- Context menus
- Selection mode
- Secondary actions
MultiFingerTransformGesture
This is just an example of a custom UIGestureRecognizer subclass that is then extended into SwiftUI. Continuous gesture combining pan, pinch, and rotation into a single transform.
@State private var transform = CGAffineTransform.identity
MultiFingerTransformGesture(
minimumNumberOfTouches: 2,
maximumNumberOfTouches: 2
)
.onChanged { recognizer in
transform = recognizer.transform
print("Translation: \(recognizer.translation)")
print("Rotation: \(recognizer.rotation)")
print("Scale: \(recognizer.scale)")
}
Use Cases:
- Photo editing
- Object manipulation
- Canvas interactions
Advanced Usage
Customizing Gesture Delegate Behavior
All gestures support full UIGestureRecognizerDelegate customization via optional closures.
Simultaneous Gesture Recognition
Allow multiple gestures to recognize at the same time:
MultiFingerPanGesture(minimumNumberOfTouches: 2, maximumNumberOfTouches: 2)
.shouldRecognizeSimultaneouslyWith { otherGesture in
// Allow simultaneous recognition with pinch gestures
return otherGesture is UIPinchGestureRecognizer
}
Default: true (allows simultaneous recognition by default)
Controlling Gesture Begin
Conditionally allow or prevent gestures from starting:
@State private var isGestureEnabled = true
MultiFingerPanGesture(minimumNumberOfTouches: 2, maximumNumberOfTouches: 2)
.shouldBegin { recognizer in
return isGestureEnabled
}
Touch and Event Filtering
Control which touches or events the gesture responds to:
MultiFingerPanGesture(minimumNumberOfTouches: 2, maximumNumberOfTouches: 2)
.shouldReceiveTouch { recognizer, touch in
// Only respond to touches inside specific views
guard let view = touch.view else { return false }
return view.tag == 100
}
Gesture Failure Dependencies
Require one gesture to fail before another begins:
let tapGesture = MultiFingerTapGesture(numberOfTouchesRequired: 1, numberOfTapsRequired: 2)
let panGesture = MultiFingerPanGesture(minimumNumberOfTouches: 1, maximumNumberOfTouches: 1)
.shouldRequireFailureOf { otherGesture in
// Pan only starts if double-tap fails
return otherGesture is UITapGestureRecognizer
}
Complete Delegate Customization Example
MultiFingerPanGesture(
minimumNumberOfTouches: 2,
maximumNumberOfTouches: 2,
shouldBegin: { recognizer in
// Only allow if conditions are met
return someCondition
},
shouldRecognizeSimultaneouslyWith: { otherGesture in
// Allow with pinch, block others
return otherGesture is UIPinchGestureRecognizer
},
shouldReceiveTouch: { recognizer, touch in
// Filter touches by location
let location = touch.location(in: touch.view)
return location.x > 100
}
)
.onChanged { recognizer in
// Handle gesture
}
Gesture State Lifecycle
All continuous gestures follow this state machine:
.possible → .began → .changed (repeated) → .ended
↘ .cancelled
↘ .failed
Discrete gestures (tap, swipe) transition directly to .onEnded
Callbacks provided:
.onBegan- Gesture started (continuous only).onChanged- Gesture updated (continuous only).onEnded- Gesture completed (continuous and discrete)
Note: .cancelled and .failed states are not exposed as they indicate the gesture did not complete successfully.
Concurrency and Thread Safety
All gesture closures are marked @MainActor because:
✅ UIKit gesture recognizers always call delegates on the main thread
✅ SwiftUI view updates must happen on the main thread
✅ You can safely capture @MainActor isolated state (view models, etc.)
@MainActor
@Observable
class ViewModel {
var count = 0
}
let viewModel = ViewModel()
MultiFingerPanGesture(minimumNumberOfTouches: 2, maximumNumberOfTouches: 2)
.onChanged { recognizer in
viewModel.count += 1 // ✅ Safe - both are @MainActor
}
Examples
Two-Finger Scrolling Canvas
struct ScrollableCanvas: View {
@State private var offset = CGSize.zero
@Stat
