SwiftUI在macOS上的嵌套滚动视图问题

19

(如果有Apple的人看到这个,我已经提交了反馈:FB8910881)

我正在尝试构建一个SwiftUI应用程序,其布局类似于Apple Music应用程序 - 艺术家页面或者像苹果的SwiftUI教程中一样(Landmarks app)。在应用程序内部,您可以在列表项上垂直滚动,在所有行上水平滚动。

Apple Music App Scrolling

在我的应用程序中,我有一个行列表,位于ScrollView(垂直)内。行本身垂直滚动,并包含ScrollViewLazyHGrid。它在iOS上工作,但在macOS上不起作用。

在iOS上,我可以通过模拟器中的点击和拖动来垂直地滚动列表和水平地滚动行项,非常顺畅。

iOS scrolling works

然而,在macOS上,如果我在行上面,只能垂直滚动...通过魔术鼠标向上/向下/向左/向右滚动。该行的ScrollView似乎捕捉了垂直滚动...在Apple Music应用程序中,如果您在艺术家播放列表行上,则可以垂直和水平滚动。

Xcode 12.2和Xcode 12.3 beta,macOS BigSur 11.0.1

enter image description here

macOS scrolling problems

这是我的代码:

import SwiftUI

struct ScrollViewProblem: View {

    var body: some View {
        ScrollView {
            HStack {
                Text("Scrolling Problem")
                    .font(.largeTitle)
                Spacer()
            }.padding()

            ScrollRow()
            ScrollRow()
            ScrollRow()
            ScrollRow()
        }
    }
}

struct ScrollRow: View {

    let row = [
        GridItem(.flexible(minimum: 200))
    ]

    var body: some View {

        VStack {
            HStack {
                Text("Row")
                    .font(.largeTitle)
                Spacer()
            }
            ScrollView(.horizontal) {
                LazyHGrid(rows: row, spacing: 20) {
                    ForEach(1..<7) { index in
                        Rectangle()
                            .fill(Color.blue)
                            .frame(width: 200, height: 200)
                    }
                }
            }
        }
        .padding()
    }
}

struct ScrollViewProblem_Previews: PreviewProvider {
    static var previews: some View {
        ScrollViewProblem()
    }
}

嗨@Luffy - 感谢您的检查。您使用的是哪个版本的Xcode / macOS?我更新了我的描述:Xcode 12.2和Xcode 12.3 beta,macOS BigSur 11.0.1,并在我的描述中放置了一个gif...仍然存在此问题... - Denise
我也遇到了同样的问题,代码非常相似。在12.2上编译,在11.0.1上运行。 - andykent
可以确认在macOS 11.0.1和XCode 12.2上出现了这个Bug。使用SwiftUI应用程序作为生命周期。 - Nikolai
遇到了相同的问题,macOS 11.0.1,Xcode 12.2。 - siejkowski
1个回答

20

虽然我希望苹果公司能够尽快解决这个 bug,但我已经找到一个解决方法。

据我所知,问题的根本原因是鼠标滚轮滚动事件没有从内部水平滚动视图转发到外部垂直滚动视图。

让人沮丧的是,AppKit 中正好有一个方法可以解决这种情况:它指定了是否应将滚动事件转发到响应者链。它被称为“wantsForwardedScrollEvents”,并在苹果文档中有说明。尽管文档说它是用于“弹性滚动”,但这个 Stack Overflow 答案显示了如何将其用于将所有滚动事件上移响应器链。

所以我们需要做的是:

  1. 创建一个继承自 NSView 的视图,并重写 wantsForwardedScrollEvents 方法,使它返回对于垂直滚动轴来说是 true,以便将垂直滚动事件从内部水平滚动传递到外部垂直滚动。
  2. 将此 NSView 插入到 SwiftUI 视图层次结构中,放在水平滚动视图和垂直滚动视图之间。

第二点有些棘手,但幸运的是,真正令人惊叹的 SwiftUI Lab 博客提供了一个完美的教程,详细说明如何做到这一点。说真的,在使用 SwiftUI 时,这个博客已经无数次拯救了我的理智。

因此,最终代码可能看起来像这样:

// we need this workaround only for macOS
#if os(macOS) 

// this is the NSView that implements proper `wantsForwardedScrollEvents` method
final class VerticalScrollingFixHostingView<Content>: NSHostingView<Content> where Content: View {

  override func wantsForwardedScrollEvents(for axis: NSEvent.GestureAxis) -> Bool {
    return axis == .vertical
  }
}

// this is the SwiftUI wrapper for our NSView
struct VerticalScrollingFixViewRepresentable<Content>: NSViewRepresentable where Content: View {
  
  let content: Content
  
  func makeNSView(context: Context) -> NSHostingView<Content> {
    return VerticalScrollingFixHostingView<Content>(rootView: content)
  }

  func updateNSView(_ nsView: NSHostingView<Content>, context: Context) {}

}

// this is the SwiftUI wrapper that makes it easy to insert the view 
// into the existing SwiftUI view builders structure
struct VerticalScrollingFixWrapper<Content>: View where Content : View {

  let content: () -> Content
  
  init(@ViewBuilder content: @escaping () -> Content) {
    self.content = content
  }
  
  var body: some View {
    VerticalScrollingFixViewRepresentable(content: self.content())
  }
}
#endif

有了这个结构,我们可以在您的示例中这样使用它:

import SwiftUI

// just a helper to make using nicer by keeping #if os(macOS) in one place
extension View {
  @ViewBuilder func workaroundForVerticalScrollingBugInMacOS() -> some View {
    #if os(macOS)
    VerticalScrollingFixWrapper { self }
    #else
    self
    #endif
  }
}

struct ScrollViewProblem: View {
  
  var body: some View {
    ScrollView {
      HStack {
        Text("Scrolling Problem")
          .font(.largeTitle)
        Spacer()
      }.padding()
      
      ScrollRow().workaroundForVerticalScrollingBugInMacOS()
      ScrollRow().workaroundForVerticalScrollingBugInMacOS()
      ScrollRow().workaroundForVerticalScrollingBugInMacOS()
      ScrollRow().workaroundForVerticalScrollingBugInMacOS()
    }
  }
}

已在macOS 11.0.1和Xcode 12.2上验证。

[根据@marshallino16的评论更新]

在将NSView添加到您的视图层次结构中后,您可能会遇到一个SwiftUI问题。内部视图停止响应外部视图状态的变化。

使用此处的示例代码中的术语:如果ScrollViewProblem中有一个@State变量被传递到ScrollRow中,则ScrollRow并不总是在该@State变量更改时重新计算。

此问题在SwiftUI Lab博客文章的"An Unpleasant Surprise"部分中提到。

不幸的是,我没有找到直接解决@State传播问题的方法,因此目前我知道的最好解决方案(也是我自己使用的)是不要从ScrollViewProblem传递@State变量到ScrollRow

这意味着你需要将你需要传递给ScrollRow的任何信息封装在外部对象(视图模型、视图存储,或者您的架构使用的任何东西)中。然后,您可以将此对象传递给ScrollRow,让ScrollRow将其保留并将其用作@State@StateObject变量。


1
这是一个惊人的答案,我可以确认它对我也起作用。非常感谢您抽出时间分享它。唯一的问题是它导致了一些尺寸回归,使得行在滚动视图中居中。我通过将视图修饰符上移到“ZStack”上一个级别来解决这个问题,以避免混淆滚动视图。 - andykent
1
是的,引入额外的包装可能会导致之前的SwiftUI结构表现出不同的行为。毕竟,它是一个额外的视图,因此如果有特定的大小定义,那么它就不再适用于该视图。很棒,你已经成功地将其调整到了你的需求。 - siejkowski
很不幸,包装的滚动视图不再对视图模型的更改做出反应了。你有这种经历吗? - marshallino16
@marshallino16 我已更新答案以解决您遇到的问题。在您的情况下,一个解决方案可能是提取仅用于滚动行的较小视图模型,并将此较小的视图模型直接传递给滚动行。这样它可以在本地保持为 @StateObject 并用于计算主体。 - siejkowski
4
谢谢您的回答!实际上我找到了一个解决方法(不是很理想),就是每次需要更新可表示视图时都设置内容。在 updateNSView > nsView.rootView = content 中进行操作。 - marshallino16
显示剩余5条评论

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