79511282

Date: 2025-03-15 14:46:46
Score: 0.5
Natty:
Report link

By 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")
            }
        }
    }
}
Reasons:
  • Blacklisted phrase (0.5): I need
  • Long answer (-1):
  • Has code block (-0.5):
  • Self-answer (0.5):
  • Low reputation (1):
Posted by: Binglei Ma