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

iOS如何实现可通过顶部下拉关闭的非全屏界面?

iOS实现非全屏可下拉关闭界面的方案

一、官方原生方案(iOS 15+ 推荐)

使用UISheetPresentationController,这是苹果官方提供的底部弹窗控制器,天然支持下拉关闭、多尺寸切换等特性,无需自定义手势。

实现步骤:

  1. 创建需要展示的内容视图控制器
  2. 配置sheet的显示参数:
    • 设置detents指定可切换的尺寸(如.medium()中等高度、.large()全屏,也可自定义高度)
    • 开启prefersGrabberVisible显示顶部抓手,提示用户可拖动
    • 可选设置largestUndimmedDetentIdentifier,让非全屏状态下背景保持清晰
  3. 以present方式展示控制器

代码示例:

// 创建内容控制器
let contentVC = YourContentViewController()

// 配置Sheet参数
if let sheet = contentVC.sheetPresentationController {
    // 支持中等高度和全屏两种尺寸
    sheet.detents = [.medium(), .large()]
    // 显示顶部拖动抓手
    sheet.prefersGrabberVisible = true
    // 中等高度时背景不模糊
    sheet.largestUndimmedDetentIdentifier = .medium
    // 内容滚动到顶部时不自动展开全屏
    sheet.prefersScrollingExpandsWhenScrolledToEdge = false
}

// 展示弹窗
present(contentVC, animated: true)

二、自定义手势实现(兼容iOS 15以下或高度定制场景)

如果需要兼容低版本系统,或者要完全自定义弹窗的样式和拖动逻辑,可以通过UIPanGestureRecognizer实现下拉关闭。

实现思路:

  1. 创建一个容器视图作为弹窗主体,布局为非全屏样式(如底部向上弹出,高度占屏幕2/3)
  2. 给容器视图添加拖动手势
  3. 在手势回调中处理拖动逻辑:
    • 拖动过程中实时更新容器视图的位置
    • 拖动结束时,根据拖动距离或速度判断是否关闭弹窗,否则回弹到原位置

代码示例:

class CustomBottomSheetVC: UIViewController {
    private let containerView = UIView()
    // 记录容器视图初始Y坐标
    private var initialContainerY: CGFloat = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        addPanGesture()
    }
    
    private func setupUI() {
        // 设置背景半透明
        view.backgroundColor = UIColor.black.withAlphaComponent(0.3)
        
        // 配置容器视图样式
        containerView.backgroundColor = .white
        containerView.layer.cornerRadius = 16
        containerView.clipsToBounds = true
        view.addSubview(containerView)
        
        // 布局容器视图为底部弹出的非全屏样式
        containerView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            containerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 2/3)
        ])
        
        // 记录初始位置
        initialContainerY = containerView.frame.origin.y
    }
    
    private func addPanGesture() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        containerView.addGestureRecognizer(panGesture)
    }
    
    @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)
        
        switch gesture.state {
        case .began:
            // 手势开始时重新记录初始位置
            initialContainerY = containerView.frame.origin.y
        case .changed:
            // 拖动过程中更新容器位置,限制不能向上拖动超过初始位置
            let newY = initialContainerY + translation.y
            containerView.frame.origin.y = max(initialContainerY, newY)
        case .ended, .cancelled:
            let velocity = gesture.velocity(in: view)
            // 判断关闭条件:拖动距离超过100pt,或向下拖动速度超过500pt/s
            let shouldDismiss = translation.y > 100 || velocity.y > 500
            
            if shouldDismiss {
                // 动画滑出屏幕后关闭控制器
                UIView.animate(withDuration: 0.3) {
                    self.containerView.frame.origin.y = self.view.bounds.height
                } completion: { _ in
                    self.dismiss(animated: false)
                }
            } else {
                // 动画回弹到初始位置
                UIView.animate(withDuration: 0.3) {
                    self.containerView.frame.origin.y = self.initialContainerY
                }
            }
        default:
            break
        }
    }
}

三、第三方库方案(可选)

如果不想重复造轮子,可以使用成熟的第三方库,比如PopupController、MDCBottomSheet等,这些库封装了完整的底部弹窗逻辑,支持下拉关闭、样式定制等功能。


内容的提问来源于stack exchange,提问作者Metin SevTHP

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 09:21:01