I'm working on a macos software and I found that in swiftui the scrollview is set to .horizontal
, so the list doesn't scroll when the mouse wheel scrolls, but it does in .vertical
mode.
But I really need this feature.
So I tried one option:implementation NSViewRepresentable
to make a NSView override scrollWheel
method that can handle mouse wheel scroll event. then post a notification.
struct MouseWheelScrollEventView : NSViewRepresentable {
class MouseView : NSView {
override var acceptsFirstResponder: Bool {
true
}
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
return true
}
override func scrollWheel(with event: NSEvent) {
NotificationCenter.default.post(name: Notification.Name("mouseevent"), object: event)
}
}
func makeNSView(context: Context) -> some NSView {
let view = MouseView()
DispatchQueue.main.async {
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSViewType, context: Context) {
print("update")
}
}
then in SwiftUI, the code like this:
@available(OSX 11.0, *)
struct ContentView: View {
let mouseWheelScrollEventPublisher = NotificationCenter.default.publisher(for: Notification.Name(rawValue: "mouseevent"))
@State var deltaY = 0.0
@State var currentIndex = 1
var body: some View{
ZStack{
ScrollView(.horizontal,showsIndicators:false) {
ScrollViewReader { value in
LazyHStack(alignment: .top) {
ForEach(1...100, id: \.self) { index in
if(self.currentIndex == index) {
Text("\(String(index))")
.frame(width: 80, height: 80)
.background(Color.yellow)
.onTapGesture(perform: {
print(index)
})
}else {
Text("\(String(index))")
.frame(width: 80, height: 80)
.background(Color.blue)
.onTapGesture(perform: {
print(index)
})
}
}
}
.onReceive(mouseWheelScrollEventPublisher) { output in
let event = output.object as! NSEvent
let deltaY = event.deltaY
if(deltaY < 0 && self.currentIndex < 100) {
self.currentIndex += 1
value.scrollTo(currentIndex)
}else if(deltaY > 0 && self.currentIndex > 1) {
self.currentIndex -= 1
value.scrollTo(currentIndex)
}
}
}
}
.frame(width: 600)
MouseWheelScrollEventView()
}
}
}
now the scroll event can handle in .horizontal
scrollView, but there is a new question: the scrollViewItem can not handle the click event, because MouseWheelScrollEventView
handle the mouse click event. But I want the scrollViewItem also can handle the mouse click event.
How can I hanle both mouse wheel scroll event and click event?
ps: I know that using appkit can solve the problem, but is there any way to try to use swiftui implementation?
You can simply listen to the app's currentEvent
and skip a snooping NSViewRep when:
Below is an example from a transient popover in a macOS SwiftUI App of mine. A horizontal ScrollView presents options, which can be "scroll selected" by vertical scrolling slowly or quickly. You could plug ScrollViewProxy in for the same idea.
In a test implementation, using .onReceive() {} would recalculate the view and a single mouse scroll would trigger repeatedly. Storing the subscription in an @State var lets it work as desired.
//horizontal scroll view
.onAppear { trackScrollWheel() }
func trackScrollWheel() {
NSApp.publisher(for: \.currentEvent)
.filter { event in event?.type == .scrollWheel }
.throttle(for: .milliseconds(200),
scheduler: DispatchQueue.main,
latest: true)
.sink { [weak vm] event in
vm?.goBackOrForwardBy(delta: Int(event?.deltaY ?? 0))
}
.store(in: &subs)
}
@State var subs = Set<AnyCancellable>() // Cancel onDisappear
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With