I have a List that contains some views, some of which need horizontal scrolling. It is important to note that none of the views require vertical scrolling. When my mouse hovers over the views that do not require horizontal scrolling, the List works normally and can scroll vertically. However, when my mouse hovers over the views that require horizontal scrolling for a moment, I am unable to scroll the List vertically; I can only scroll horizontally. If I replace the List with a ScrollView, everything works perfectly, and both scrollings function well. However, ScrollView(including LazyVSatck) has serious performance issues in my App, so I had to turn to List. Although macOS 13+ provides .scrollDisabled(true)
to disable scrolling, it is effective, but it disables both horizontal and vertical scrolling simultaneously, which is not what I want. The last part of the video shows my attempt to scroll vertically, which does not work.
Example video
Here is the minimal reproducible code.
import SwiftUI
struct ContentView: View {
var body: some View {
List {
ForEach(0..<5) { index in
Text("\(index) line")
}
ScrollView(.horizontal) {
Text("hello, world! hello, world! hello, world! hello, world! hello, world!\n hello, world! hello, world! hello, world! \n hello, world! hello, world! hello, world! hello, world!")
}.font(.largeTitle)
ForEach(5..<10) { index in
Text("\(index) line")
}
}
}
}
#Preview {
ContentView()
}
I expecting the List to receive vertical scrolling events no matter what.
I have a List that contains some views, some of which need horizontal scrolling. It is important to note that none of the views require vertical scrolling. When my mouse hovers over the views that do not require horizontal scrolling, the List works normally and can scroll vertically. However, when my mouse hovers over the views that require horizontal scrolling for a moment, I am unable to scroll the List vertically; I can only scroll horizontally. If I replace the List with a ScrollView, everything works perfectly, and both scrollings function well. However, ScrollView(including LazyVSatck) has serious performance issues in my App, so I had to turn to List. Although macOS 13+ provides .scrollDisabled(true)
to disable scrolling, it is effective, but it disables both horizontal and vertical scrolling simultaneously, which is not what I want. The last part of the video shows my attempt to scroll vertically, which does not work.
Example video
Here is the minimal reproducible code.
import SwiftUI
struct ContentView: View {
var body: some View {
List {
ForEach(0..<5) { index in
Text("\(index) line")
}
ScrollView(.horizontal) {
Text("hello, world! hello, world! hello, world! hello, world! hello, world!\n hello, world! hello, world! hello, world! \n hello, world! hello, world! hello, world! hello, world!")
}.font(.largeTitle)
ForEach(5..<10) { index in
Text("\(index) line")
}
}
}
}
#Preview {
ContentView()
}
I expecting the List to receive vertical scrolling events no matter what.
Share edited Mar 7 at 16:34 Binglei Ma asked Mar 7 at 15:37 Binglei MaBinglei Ma 214 bronze badges 2- Seems related to this one which has some workarounds: stackoverflow/questions/64920744/… – drseg Commented Mar 8 at 16:19
- Thank you @drseg, I got inspiration from the link you provided and also referenced this post. – Binglei Ma Commented Mar 15 at 13:59
1 Answer
Reset to default 1By referring to this post and this post, I got my workaround.
Although my issue has been resolved, I am still waiting for a native SwiftUI solution. This is just a workaround.
1. First, based on this answer, I need to implement my own NSScrollView()
and override the scrollWheel()
method to forward vertical scroll events. This answer also forwards vertical scroll events by overriding wantsForwardedScrollEvents()
, but it is "too sensitive". Users' fingers cannot scroll precisely horizontally; there will always be a certain distance generated on the y-axis. Therefore, I did not adopt that method, even though it seems to be "less intrusive".
class MTHorizontalScrollView: NSScrollView {
var currentScrollIsHorizontal = false
override func scrollWheel(with event: NSEvent) {
if event.phase == NSEvent.Phase.began || (event.phase == NSEvent.Phase.ended && event.momentumPhase == NSEvent.Phase.ended) {
currentScrollIsHorizontal = abs(event.scrollingDeltaX) > abs(event.scrollingDeltaY)
}
if currentScrollIsHorizontal {
super.scrollWheel(with: event)
} else {
self.nextResponder?.scrollWheel(with: event)
}
}
}
2. I need to create an NSViewRepresentable
to use it in SwiftUI.
struct NSScrollViewWrapper<Content: View>: NSViewRepresentable {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeNSView(context: Context) -> NSScrollView {
let scrollView = MTHorizontalScrollView()
scrollView.hasHorizontalScroller = true
scrollView.hasVerticalScroller = false
scrollView.verticalScrollElasticity = .none
scrollView.horizontalScrollElasticity = .allowed
scrollView.autohidesScrollers = true
scrollView.drawsBackground = false
let hostingView = NSHostingView(rootView: content)
hostingView.translatesAutoresizingMaskIntoConstraints = false
scrollView.documentView = hostingView
NSLayoutConstraint.activate([
hostingView.topAnchor.constraint(equalTo: scrollView.topAnchor),
hostingView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
hostingView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
hostingView.widthAnchor.constraint(greaterThanOrEqualTo: scrollView.widthAnchor)
])
return scrollView
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
if let hostingView = nsView.documentView as? NSHostingView<Content> {
hostingView.rootView = content
}
}
}
3. Perhaps completing step 2 is sufficient for use, but here I will take it a step further and extend it to View
for easier use anywhere.
extension View {
@ViewBuilder
func forwardedScrollEvents(_ enabled: Bool = true) -> some View {
if enabled {
NSScrollViewWrapper {
self
.scrollDisabled(true)
.frame(maxWidth: .infinity, alignment: .leading)
}
} else {
self
}
}
}
4. Everything is ready, and it can be used now.
struct ContentView: View {
var body: some View {
List {
ForEach(0..<5) { index in
Text("\(index) line")
}
ScrollView(.horizontal) {
Text("hello, world! hello, world! hello, world! hello, world! hello, world!\n hello, world! hello, world! hello, world! \n hello, world! hello, world! hello, world! hello, world!")
}.font(.largeTitle)
.forwardedScrollEvents() //this!
ForEach(5..<10) { index in
Text("\(index) line")
}
}
}
}