SwiftUI: Two-finger swipe ( scroll ) gesture
import Combine
@main
struct MyApp: App {
@State var subs = Set<AnyCancellable>() // Cancel onDisappear
@SceneBuilder
var body: some Scene {
WindowGroup {
SomeWindowView()
/////////////
// HERE!!!!!
/////////////
.onAppear { trackScrollWheel() }
}
}
}
/////////////
// HERE!!!!!
/////////////
extension MyApp {
func trackScrollWheel() {
NSApp.publisher(for: \.currentEvent)
.filter { event in event?.type == .scrollWheel }
.throttle(for: .milliseconds(200),
scheduler: DispatchQueue.main,
latest: true)
.sink {
if let event = $0 {
if event.deltaX > 0 { print("right") }
if event.deltaX < 0 { print("left") }
if event.deltaY > 0 { print("down") }
if event.deltaY < 0 { print("up") }
}
}
.store(in: &subs)
}
}
Add vertical two finger swipe gesture to UIScrollView
When you add the UISwipeGestureRecognizer
to your window, keep a reference to it, so you can access it later via the AppDelegate
:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var twoFingerSwipeDownRecognizer: UISwipeGestureRecognizer?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let twoFingerSwipeDownRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(didRecognizeTwoFingerSwipeDown))
twoFingerSwipeDownRecognizer.numberOfTouchesRequired = 2
twoFingerSwipeDownRecognizer.direction = .down
window?.addGestureRecognizer(twoFingerSwipeDownRecognizer)
self.twoFingerSwipeDownRecognizer = twoFingerSwipeDownRecognizer
return true
}
@objc func didRecognizeTwoFingerSwipeDown(recognizer: UISwipeGestureRecognizer) {
print("SWIPE DOWN")
}
}
Then, in the UIViewController
that contains the UITableView
(or UIScrollView
) you have to call require(toFail:)
on the UITableView's pan gesture recognizer:
func enableTwoFingerSlideDown() {
guard
let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let twoFingerGestureRecognizer = appDelegate.twoFingerSwipeDownRecognizer
else {
return
}
tableView.panGestureRecognizer.require(toFail: twoFingerGestureRecognizer)
}
Now the two finger down gesture works over a UITableView
.
UPDATE
Because a swipe is a discrete gesture the solution above might not be the perfect solution. When you scroll down slowly with one finger you will notice that the UITableView
will not scroll down instantly. There is a short delay because the UITableView's
UIPanGestureDelagate
has to wait until the (Two Finger) SwipeDelegate has failed. And that takes some time.
A better solution might be to use a UIPanGestureRecognizer
to recognize a two finger pan and then disable the scrolling on the UITableView
while the user is panning using two fingers.
That could be achieved like this:
In your AppDelegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var twoFingerPanRecognizer: UIPanGestureRecognizer?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let twoFingerSwipeDownRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didRecognizeTwoFingerPan))
twoFingerSwipeDownRecognizer.minimumNumberOfTouches = 2
twoFingerSwipeDownRecognizer.maximumNumberOfTouches = 2
twoFingerPanRecognizer?.delegate = self
window?.addGestureRecognizer(twoFingerSwipeDownRecognizer)
self.twoFingerPanRecognizer = twoFingerSwipeDownRecognizer
return true
}
@objc func didRecognizeTwoFingerPan(recognizer: UIPanGestureRecognizer) {
let tableView = recognizer.view as? UITableView
switch recognizer.state {
case .began:
tableView?.isScrollEnabled = false
case .changed:
let swipeThreshold: CGFloat = 50
switch recognizer.translation(in: nil).y {
case ...(-swipeThreshold):
print("Swipe UP")
recognizer.isEnabled = false
case swipeThreshold...:
print("Swipe DOWN")
recognizer.isEnabled = false
default:
break
}
case .cancelled, .ended, .failed, .possible:
tableView?.isScrollEnabled = true
recognizer.isEnabled = true
}
}
}
extension AppDelegate: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
In your ViewController:
func enableTwoFingerSlideDown() {
guard
let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let twoFingerGestureRecognizer = appDelegate.twoFingerPanRecognizer
else {
return
}
tableView.addGestureRecognizer(twoFingerGestureRecognizer)
}
You have to take the UIPanGestureRecognizer
from the AppDelegate
and add it to the UITableView
. Otherwise this won't work. Just remember to add it back to the UIWindow
before this UIViewController
is dismissed.
With this solution the normal scrolling behavior of the UITableView
remains unchanged.
Catch two finger double click in SwiftUI & VoiceOver?
I found out myself. This gesture is called "Magic Tap" and you can intercept it in SwiftUI like this:
.accessibilityAction(.magicTap, {
// process 2-finger double click
})
Recognize UISwipeGestureRecognizer with two-finger scroll on trackpad
While I didn't find a way to do this with UISwipeGestureRecognizer
, I solved it by adding a UIPanGestureRecognizer
. There's a few things to be aware of.
You need to allow it to be triggered with continuous (trackpad) and discrete (mouse scroll wheel) scrolling:
panGestureRecognizer.allowedScrollTypesMask = [.continuous, .discrete]
To ensure it's only triggered when scrolling down for example, you can implement this delegate method to check which direction they scrolled:
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == panGestureRecognizer {
//only support scrolling down to close
if let view = panGestureRecognizer.view {
let velocity = panGestureRecognizer.velocity(in: view)
return velocity.y > velocity.x
}
}
return true
}
One other gotcha, the pan will be triggered when swiping with your finger on the display. You can prevent the pan gesture from being recognized with direct touches so it's only triggered via trackpad/mouse by implementing this other delegate method:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if gestureRecognizer == panGestureRecognizer {
return false //disallow touches, only allow trackpad/mouse scroll
}
return true
}
How to implement the two-finger swipe gesture in Cocoa to go back and forward?
Three finger swipes are easiest, because NSResponder already does the work for you:
- (void)swipeWithEvent:(NSEvent *)event;
If you want to support two finger swipes (which I don't think technically can be classified as swipes, but rather scroll gestures), you'll have to manually process the touches- see http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/EventOverview/HandlingTouchEvents/HandlingTouchEvents.html#//apple_ref/doc/uid/10000060i-CH13-SW10
SwiftUI ScrollView with Tap and Drag gesture
I tried a few option and I think a combination of sequenced
and simultaneously
allows two gestures to run the same time. To achieve a onTouchDown I used a DragGesture
with minimum distance of 0.
struct ContentView: View {
var body: some View {
ScrollView() {
ForEach(0..<5, id: \.self) { i in
ListElem()
.frame(maxWidth: .infinity)
}
}
}
}
struct ListElem: View {
@State private var offset = CGSize.zero
@State private var isDragging = false
@GestureState var isTapping = false
var body: some View {
// Gets triggered immediately because a drag of 0 distance starts already when touching down.
let tapGesture = DragGesture(minimumDistance: 0)
.updating($isTapping) {_, isTapping, _ in
isTapping = true
}
// minimumDistance here is mainly relevant to change to red before the drag
let dragGesture = DragGesture(minimumDistance: 0)
.onChanged { offset = $0.translation }
.onEnded { _ in
withAnimation {
offset = .zero
isDragging = false
}
}
let pressGesture = LongPressGesture(minimumDuration: 1.0)
.onEnded { value in
withAnimation {
isDragging = true
}
}
// The dragGesture will wait until the pressGesture has triggered after minimumDuration 1.0 seconds.
let combined = pressGesture.sequenced(before: dragGesture)
// The new combined gesture is set to run together with the tapGesture.
let simultaneously = tapGesture.simultaneously(with: combined)
return Circle()
.overlay(isTapping ? Circle().stroke(Color.red, lineWidth: 5) : nil) //listening to the isTapping state
.frame(width: 100, height: 100)
.foregroundColor(isDragging ? Color.red : Color.black) // listening to the isDragging state.
.offset(offset)
.gesture(simultaneously)
}
}
For anyone interested here is a custom scroll view that will not be blocked by other gestures as mentioned in one of the comments. As this was not possible to be solved with the standard ScrollView.
OpenScrollView for SwiftUI on Github
Credit to
https://stackoverflow.com/a/59897987/12764795
http://developer.apple.com/documentation/swiftui/composing-swiftui-gestures
https://www.hackingwithswift.com/books/ios-swiftui/how-to-use-gestures-in-swiftui
Related Topics
Swift: Nsstatusitem Menu Behaviour in 10.10 (E.G. Show Only on Right Mouse Click)
Search Multiple Words in One String in Swift
Ckcontainer.Discoverallidentities Always Fails
How to Import Modules Without an Xcode Project in Swift
Swiftui: Using View Modifiers Between Different iOS Versions Without #Available
Xcode 8 Shell Script Invocation Error
Swift Safely Unwrapping Optinal Strings and Ints
How to Copy Skspritenode with Skphysicsbody
Context Menu Not Updating in Swiftui
How to Hash a String to Sha512 in Swift
iOS 13 Modals - Calling Swipe Dismissal Programmatically
Limit Rectangle to Screen Edge on Drag Gesture
Is There a Technical Reason to Use Swift's Caseless Enum Instead of Real Cases
Glkit VS. Metal Perspective Matrix Difference
How to Use Enumeratedate in Swift 3 to Find All Sundays the Last 50 Years