You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI自定义BottomSheet拖拽至屏幕底部附近自动关闭实现咨询

实现方案

我们采用动态读取内容高度设置关闭阈值的方案,适配任意外部传入的内容尺寸,具体修改如下:

核心修改点

  • 新增PreferenceKey用于读取BottomSheet内容的实际高度
  • 新增存储内容高度的状态变量,在布局阶段读取传入内容的高度
  • 调整拖拽手势逻辑,拖拽偏移超过内容高度的30%(可自定义)时自动关闭组件

完整修改后代码

import SwiftUI

struct ContentView: View {
  @State private var isShow = false

  var body: some View {
    ZStack {
      Button(
        "Show Sheet",
        action: {
          self.isShow.toggle()
        }
      )
      .zIndex(0)
      .allowsHitTesting(!isShow)

      BottomSheet(
        isShow: self.$isShow,
        content: {
          VStack {
            Text("A")
            Text("B")
            Text("C")
            Text("A")
            Text("B")
            Text("C")
            Text("A")
            Text("B")
            Text("C")
          }
          .frame(
            maxWidth: .infinity
          )
          .background(Color(.yellow))
        }
      )
      .zIndex(1)
    }
  }
}

struct ScrimView: View {
  var body: some View {
    VStack {}.frame(
      maxWidth: .infinity,
      maxHeight: .infinity,
      alignment: .bottom
    )
    .background(
      Color(.gray)
    )
    .opacity(0.5)
  }
}

// 新增PreferenceKey用于读取内容高度
private struct ContentHeightKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

struct BottomSheet<Content: View>: View {
  private let content: () -> Content
  @Binding var isShow: Bool
  @State var offsetY: CGFloat = 0
  // 新增存储内容高度的状态变量
  @State private var contentHeight: CGFloat = 0

  init(
    isShow: Binding<Bool>,
    content: @escaping () -> Content
  ) {
    self._isShow = isShow
    self.content = content
  }

  var body: some View {
    ZStack(alignment: .bottom) {
      if self.isShow {
        ScrimView().zIndex(
          0
        )
        .onTapGesture {
          self.isShow = false
        }
        .transition(.opacity)

        VStack {
          Button(
            "X",
            action: {
              self.isShow = false
            }
          )
          self.content()
          // 新增:读取content的高度
          .background(
            GeometryReader { geo in
                Color.clear.preference(key: ContentHeightKey.self, value: geo.size.height)
            }
          )
        }
        .zIndex(1)
        .background(Color(.white))
        .cornerRadius(10)
        .offset(y: self.offsetY)
        .gesture(self.gesture)
        .transition(.move(edge: .bottom))
        // 新增:接收读取到的内容高度
        .onPreferenceChange(ContentHeightKey.self) { height in
            contentHeight = height
        }
      }
    }
    .animation(.default, value: isShow)
    .onChange(
      of: self.isShow,
      perform: { value in
        self.offsetY = 0
      }
    )
  }

  var gesture: some Gesture {
    DragGesture().onChanged{ value in
      guard value.translation.height > 0 else { return }
      self.offsetY = value.translation.height
    }
    .onEnded{ value in
      guard value.translation.height > 0 else { return }
      // 拖拽偏移超过内容高度30%则关闭,阈值可自行调整
      if value.translation.height > contentHeight * 0.3 {
          withAnimation {
              isShow = false
          }
      } else {
          withAnimation {
              offsetY = 0
          }
      }
    }
  }
}

可选方案(基于屏幕位置判断)

如果需要基于屏幕位置判断关闭阈值,可直接通过UIScreen.main.bounds.height获取屏幕高度,结合拖拽偏移量计算是否到达屏幕底部,不过该方案适配不同尺寸设备、安全区的成本更高,优先推荐使用内容高度判断的方案。


内容的提问来源于stack exchange,提问作者shingo.nakanishi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.10 03:39:04