返回

SwiftUI Sheet 滚动视图下拉关闭:终极解决方案

IOS

SwiftUI Sheet 中带滚动视图的挑战与解决方案

在 SwiftUI 中,创建一个类似 Apple 地图中那样的可调整大小的 Sheet,并允许其内部包含可滚动内容,看似简单,但实践中可能会遇到一些棘手问题。其中一个常见问题就是,当 Sheet 中包含 ScrollView 时,Sheet 的手势下拉关闭功能可能受到影响,用户无法如预期般下拉关闭 Sheet。尤其当 Sheet 同时包含 TabView 等复杂视图时,情况会变得更加复杂。本文将探讨这一问题并提供有效的解决方案。

问题分析:滚动视图与 Sheet 手势冲突

ScrollView 占据了 Sheet 的大部分区域时,用户的手势操作很可能首先被 ScrollView 的滚动行为捕获,而不是传递给 Sheet 的下拉手势。这就导致无法直接通过下拉手势关闭 Sheet。特别是在嵌套使用 TabView 的场景下,这个问题变得更为明显。TabView 会干扰 ScrollView 手势以及Sheet手势的传递,使整体操作体验受损。

PresentationDetent 虽然能帮助控制Sheet的展开高度,但在频繁切换或与ScrollView交互时,可能出现视觉上的跳跃或不连贯,这通常是由其内部手势处理机制的差异造成的。

解决方案一:使用手势识别器优化ScrollView行为

为了让用户可以下拉关闭Sheet,同时也保留滚动视图,可使用 SwiftUI 的 gesture 修饰器来限制 ScrollView 在垂直方向的滚动。通过检查 contentOffset,在 contentOffset 为顶端的时候,停止 ScrollView 滚动。这个方案需要借助CoordinateSpace 获取ScrollView的偏移量,以做出准确的判断。

操作步骤:

  1. 添加 @State 变量来存储偏移量。
  2. ScrollView 嵌入 GeometryReader 中。
  3. GeometryReaderonPreferenceChange 属性中获取ScrollView 的滚动偏移。
  4. 判断偏移量,小于0时不滚动。

代码示例:

import SwiftUI

struct ScrollViewWithPullDown: View {
    @State private var contentOffset: CGFloat = 0
     
     var body: some View {
         GeometryReader { geometry in
             ScrollView {
                
                Color.clear.frame(width:geometry.size.width ,height: 0.01).background(GeometryReader{ gp in
                   Color.clear.preference(key: PreferenceOffset.self, value: gp.frame(in:.global).minY)
                       .onPreferenceChange(PreferenceOffset.self){
                           contentOffset = $0
                        }
                 } )
                 
               
                 LazyVStack{
                 ForEach(1...300, id: \.self) { i in
                     Text("文本内容 \(i)").padding(4)
                 }
            }
                
         }
         .gesture(DragGesture().onChanged{ value in
            if contentOffset < 0 {
             
             
             // 手势回调
         }

         })
     }
    }
}

struct PreferenceOffset:PreferenceKey{
    static var defaultValue:CGFloat = .zero
   static func reduce(value: inout CGFloat, nextValue: () -> CGFloat){
       value = nextValue()
    }
    
}

这个方案可以有效处理 Sheet 的下拉关闭,并且保留了ScrollView 的滚动功能。但是,在 TabView 中使用此方法,效果会变差,TabView仍然可能会阻碍 Sheet 的下拉操作。这是因为 TabView 本身也涉及到手势处理。

解决方案二:调整 TabViewScrollView 的结构

如果 TabViewScrollView 同时使用,简单的手势处理往往效果不佳,可以尝试调整结构,将 ScrollView 作为 TabView 中的每一个 Page View的父视图,避免多个滚动视图相互干扰,并为 ScrollView 使用新的可配置的关闭处理机制。

操作步骤:

  1. 放弃原有的ScrollView结构。
  2. ForEach 里面的每一个 Tab 使用一个 ScrollviewWithPullDown View.

代码示例:


struct TabbedSheetView: View {
    @Binding var isSheetPresented: Bool
    var body: some View {
           TabView {
               ForEach(0..<5, id: \.self) { i in
                     ScrollViewWithPullDown()
               }
           }
           .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
    }
}

这样结构的好处是,每一个 ScrollView 都独立的处理滑动逻辑,不受其他元素干扰。同时 Sheet 下拉关闭的逻辑则没有发生改变。如果用户希望滑动 Tab,依然可以通过水平滑动来实现。

代码说明:

ScrollViewWithPullDownScrollView 手势进行了自定义的处理。 只有偏移量到达顶部或者上边时才会让ScrollView手势生效,其它时候,允许 sheet 下滑操作关闭。通过分离 TabView 的每一项为一个独立的滑动容器,解决了原有手势冲突的问题,从而使得下拉关闭功能恢复正常。

额外说明:

  • 以上方法,本质上是将原本同时响应用户操作的复杂手势简化为可控制的,逐个处理手势的过程。
  • 如果滑动过程用户期望的是平滑的关闭效果,建议结合 Animation 实现一个类似Apple Maps 的关闭体验。

通过上述调整,即可在 SwiftUI 中创建一个类似于 Apple Maps 中具有流畅下拉关闭效果且包含 ScrollViewTabView 的 Sheet。