Swiftui: Two-Finger Swipe ( Scroll ) Gesture

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



Leave a reply



Submit