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

Den*_*ise 15 macos ios swift swiftui

(如果任何苹果人看到这个 - 我提交了一个反馈:FB8910881)

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

Apple Music 应用滚动

在我的应用程序中,我有一个ScrollView(垂直)内的行列表。行本身垂直滚动并包含 aScrollView和 an LazyHGrid。它适用于 iOS,但不适用于 macOS。

在 iOS 上,我可以垂直滚动列表并水平滚动行项目 - 在模拟器中,您单击并拖动以在应用程序内滚动 - 就像一个魅力。

iOS滚动工作

但是,在macOS 上,如果我不在一行上,我只能垂直滚动……我通过魔术鼠标向上/向下/向左/向右滚动。该行的 Scrollview 似乎抓住了垂直滚动...在 Apple Music 应用程序中,如果您在艺术家播放列表行上,您可以上下左右滚动。

Xcode 12.2 和 Xcode 12.3 测试版,macOS BigSur 11.0.1

在此处输入图片说明

macOS 滚动问题

这是我的代码:

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

sie*_*ski 15

我已经找到了解决该错误的方法,但我希望 Apple 能尽快解决该问题。

据我所知,潜在的问题是鼠标滚轮滚动事件不会从内部水平滚动视图转发到外部垂直滚动视图。

让它如此令人沮丧的是,有一种AppKit方法正好适用于该场景:指定滚动事件是否应沿响应者链向上转发。它被调用wantsForwardedScrollEvents并记录在 Apple 文档中。即使文档说它是用于“弹性滚动”,这个堆栈溢出答案显示了它对于将事件传递到响应器链的所有滚动事件同样有用。

所以我们需要的是:

  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
Run Code Online (Sandbox Code Playgroud)

准备好该结构后,我们可以在您的示例中使用它,如下所示:

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()
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

在 macOS 11.0.1 和 Xcode 12.2 上验证。

[根据@marshallino16 的评论更新]

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

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

SwiftUI Lab 博客文章“令人不快的惊喜”部分提到了这个问题。

不幸的是,我还没有找到使@State传播再次起作用的直接解决方法,因此目前我知道(并且一直在使用我自己)的最佳工作解决方案是不要将@State变量从ScrollViewProblemto传递给ScrollRow.

这意味着无论您需要传递给 什么信息ScrollRow,您都必须将其封装在外部对象(视图模型、视图存储,无论您的架构使用什么)中。然后您可以将此对象传递给ScrollRow并让其ScrollRow保留并将其用作变量@State@StateObject变量。

  • 感谢您的解答!我实际上找到了一种解决方法(并不理想),每次需要更新可表示视图时我都会设置内容。在 **updateNSView** &gt; `nsView.rootView = content` (3认同)
  • 这是一个了不起的答案,可以证实它也适合我。非常感谢您花时间分享。唯一的问题是,它会导致一些大小回归,使行在滚动视图中居中。我通过将视图修饰符向上移动到“ZStack”上来解决这个问题,以避免混淆滚动视图。 (2认同)