SwiftUI:双指滑动(滚动)手势

5

我对双指滑动(滚动)手势很感兴趣。

不是双指拖动,而是双指滑动(不需要按下)。就像在Safari中用于上下滚动一样。

因为我发现基本手势中没有任何一个适合此操作:TapGesture-不是;LongPressGesture-不是;DragGesture-不是;MagnificationGesture-不是;RotationGesture-不是;

有人有什么想法如何实现这个功能吗?

我至少需要一个方向来参考。


  • 这是MacOS项目
  • 顺便说一下,我不能在我的项目中使用UI类,也不能将项目重新制作为Catalyst

这可能有点老了,但可以指导你朝着正确的方向前进:https://dev59.com/I1nUa4cB1Zd3GeqPZ2ON - koen
4个回答

6
尊重@duncan-c的回答,更有效的方法是使用NSResponder的scrollWheel(with: NSEvent)机制来跟踪两指滚动(苹果鼠标上的一个手指)。然而,它只适用于NSView,因此您需要使用NSRepresentableView将其集成到SwiftUI中。以下是一组完整可用的代码,使用委托和回调将滚动事件传回到SwiftUI中以滚动主图像:
//
//  ContentView.swift
//  ScrollTest
//
//  Created by TR Solutions on 6/9/21.
//

import SwiftUI

/// How the view passes events back to the representable view.
protocol ScrollViewDelegateProtocol {
  /// Informs the receiver that the mouse’s scroll wheel has moved.
  func scrollWheel(with event: NSEvent);
}

/// The AppKit view that captures scroll wheel events
class ScrollView: NSView {
  /// Connection to the SwiftUI view that serves as the interface to our AppKit view.
  var delegate: ScrollViewDelegateProtocol!
  /// Let the responder chain know we will respond to events.
  override var acceptsFirstResponder: Bool { true }
  /// Informs the receiver that the mouse’s scroll wheel has moved.
  override func scrollWheel(with event: NSEvent) {
    // pass the event on to the delegate
    delegate.scrollWheel(with: event)
  }
}

/// The SwiftUI view that serves as the interface to our AppKit view.
struct RepresentableScrollView: NSViewRepresentable, ScrollViewDelegateProtocol {
  /// The AppKit view our SwiftUI view manages.
  typealias NSViewType = ScrollView
  
  /// What the SwiftUI content wants us to do when the mouse's scroll wheel is moved.
  private var scrollAction: ((NSEvent) -> Void)?
  
  /// Creates the view object and configures its initial state.
  func makeNSView(context: Context) -> ScrollView {
    // Make a scroll view and become its delegate
    let view = ScrollView()
    view.delegate = self;
    return view
  }
  
  /// Updates the state of the specified view with new information from SwiftUI.
  func updateNSView(_ nsView: NSViewType, context: Context) {
  }
  
  /// Informs the representable view  that the mouse’s scroll wheel has moved.
  func scrollWheel(with event: NSEvent) {
    // Do whatever the content view wants
    // us to do when the scroll wheel moved
    if let scrollAction = scrollAction {
      scrollAction(event)
    }
  }

  /// Modifier that allows the content view to set an action in its context.
  func onScroll(_ action: @escaping (NSEvent) -> Void) -> Self {
    var newSelf = self
    newSelf.scrollAction = action
    return newSelf
  }
}

/// Our SwiftUI content view that we want to be able to scroll.
struct ContentView: View {
  /// The scroll offset -- when this value changes the view will be redrawn.
  @State var offset: CGSize = CGSize(width: 0.0, height: 0.0)
  /// The SwiftUI view that detects the scroll wheel movement.
  var scrollView: some View {
    // A view that will update the offset state variable
    // when the scroll wheel moves
    RepresentableScrollView()
      .onScroll { event in
        offset = CGSize(width: offset.width + event.deltaX, height: offset.height + event.deltaY)
      }
  }
  /// The body of our view.
  var body: some View {
    // What we want to be able to scroll using offset(),
    // overlaid (must be on top or it can't get the scroll event!)
    // with the view that tracks the scroll wheel.
    Image(systemName:"applelogo")
      .scaleEffect(20.0)
      .frame(width: 200, height: 200, alignment: .center)
      .offset(offset)
      .overlay(scrollView)
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

2
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)
    }
}

这对我来说只是偶尔有效,可能与我试图将其附加到“列表”有关。偶尔会得到一些输出,但可靠性不足以将逻辑附加到它上面,遗憾的是。 - sas
这足以在信号系统使用时附加逻辑。 - Andrew_STOP_RU_WAR_IN_UA

2

编辑:更正我的答案,涵盖Mac OS

上下滚动是使用 NSPanGestureRecognizer。它有一个 numberOfTouchesRequired 属性,可以让您通过两个手指来响应它。

Mac OS 没有滑动手势识别器。

标准的 UISwipeGestureRecognizer 正好符合您的需求。只需将 numberOfTouchesRequired 设置为 2。

...虽然我不确定移动 Safari 是否使用滑动手势。它可能是带有一些特殊编码的双指拖动。


标签“macos”。目前还没有UI类,只有NS。谷歌上没有关于NSSwipeGestureRecognizer的信息 =( 更正确的说法是无法将UI类连接到我的项目中。 - Andrew_STOP_RU_WAR_IN_UA
哦,抱歉。我太习惯于每个人都问关于iOS的问题,所以错过了那个。UI类是特定于iOS的。 - Duncan C

0
如果您的视图已经实现了NSViewRepresentable协议,您可以通过在其onAppear方法中添加以下内容来处理滚动事件:
MyRepresentableView()
.onAppear {
    NSEvent.addLocalMonitorForEvents(matching: [.scrollWheel]) { event in
        print("dx = \(event.deltaX)  dy = \(event.deltaY)")
        return event
    }
} 

和我的解决方案一样,但速度较慢 :) - Andrew_STOP_RU_WAR_IN_UA
@Andrew___Pls_Support_UA 我发现你的版本实际上比我的慢,除非我将你的油门时间从200毫秒减少到例如2毫秒,这样它看起来就像我的一样响应迅速。我是根据在Magic Trackpad(带有Mac Mini M2)上执行滚动手势和在代码中首次检测到它之间的表观时间来判断的。无论哪种情况,响应似乎都是即时的。在我的应用程序中,我不关心任何单个滚动手势生成的后续事件接收的速度有多快。你是如何测量速度的? - RG_

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接