admin管理员组

文章数量:1410697

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
Add a comment  | 

1 Answer 1

Reset to default 1

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

本文标签: