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

无需CocoaPods:如何在Root ViewController中以Bottom Sheet展示多ViewController

实现自定义Bottom Sheet ViewController(无需CocoaPods)

嘿,刚好我最近做过类似的需求,不用第三方库完全可以自己实现自定义的Bottom Sheet,下面是具体的思路和代码示例,一步步来就能搞定:

核心思路

  • 用一个容器ViewController作为Bottom Sheet的载体,把要展示的目标VC作为子控制器嵌入进去,这样就能灵活切换不同的内容
  • 通过约束动画控制容器视图的位置,初始让它藏在屏幕底部,点击按钮时滑入屏幕;关闭时滑出
  • 添加手势交互,支持拖拽关闭和点击背景关闭,提升用户体验
  • 在Root VC中管理这个容器的复用,避免重复创建,切换内容时直接替换子VC就行

步骤1:创建Bottom Sheet容器类

先写一个专门的容器类BottomSheetContainerViewController,负责处理动画、手势和内容VC的管理:

import UIKit

class BottomSheetContainerViewController: UIViewController {
    // 用来承载内容VC的容器视图
    private let contentContainer = UIView()
    // 控制Sheet位置的核心约束(初始在屏幕外)
    private var bottomConstraint: NSLayoutConstraint!
    // 当前显示的内容VC
    private var currentContentVC: UIViewController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBaseUI()
        setupInteractionGestures()
    }
    
    private func setupBaseUI() {
        // 半透明背景
        view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
        view.isUserInteractionEnabled = true
        
        // 配置内容容器
        view.addSubview(contentContainer)
        contentContainer.translatesAutoresizingMaskIntoConstraints = false
        contentContainer.backgroundColor = .white
        contentContainer.layer.cornerRadius = 16 // 顶部圆角
        contentContainer.clipsToBounds = true
        
        // 设置内容容器的约束:左右贴边,高度默认占屏幕60%
        NSLayoutConstraint.activate([
            contentContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            contentContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            contentContainer.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.6)
        ])
        
        // 初始时让内容容器完全在屏幕下方
        bottomConstraint = contentContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: UIScreen.main.bounds.height * 0.6)
        bottomConstraint.isActive = true
    }
    
    private func setupInteractionGestures() {
        // 拖拽手势:允许向下拖拽关闭Sheet
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        contentContainer.addGestureRecognizer(panGesture)
        
        // 点击背景关闭Sheet
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissBottomSheet))
        view.addGestureRecognizer(tapGesture)
        tapGesture.delegate = self
    }
    
    // 切换内容VC的方法,外部调用这个来替换显示的内容
    func setContentViewController(_ vc: UIViewController) {
        // 移除旧的内容VC
        currentContentVC?.willMove(toParent: nil)
        currentContentVC?.view.removeFromSuperview()
        currentContentVC?.removeFromParent()
        
        // 添加新的内容VC
        addChild(vc)
        vc.view.translatesAutoresizingMaskIntoConstraints = false
        contentContainer.addSubview(vc.view)
        
        // 让内容VC完全填充容器
        NSLayoutConstraint.activate([
            vc.view.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor),
            vc.view.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
            vc.view.topAnchor.constraint(equalTo: contentContainer.topAnchor),
            vc.view.bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor)
        ])
        
        vc.didMove(toParent: self)
        currentContentVC = vc
    }
    
    // 显示Bottom Sheet的动画
    func showSheet() {
        bottomConstraint.constant = 0
        UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5) {
            self.view.layoutIfNeeded()
        }
    }
    
    // 关闭Bottom Sheet的动画
    @objc private func dismissBottomSheet() {
        bottomConstraint.constant = UIScreen.main.bounds.height * 0.6
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        } completion: { _ in
            self.dismiss(animated: false)
            // 重置容器引用,下次点击重新创建(可选,根据你的需求调整)
            if let rootVC = UIApplication.shared.keyWindow?.rootViewController as? RootViewController {
                rootVC.bottomSheetContainer = nil
            }
        }
    }
    
    // 处理拖拽手势逻辑
    @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)
        // 只允许向下拖拽
        if translation.y > 0 {
            bottomConstraint.constant = translation.y
        }
        
        // 手势结束时判断是否关闭
        if gesture.state == .ended {
            let velocity = gesture.velocity(in: view)
            // 拖拽速度快或者拖拽距离超过Sheet高度的一半,就关闭
            if velocity.y > 300 || bottomConstraint.constant > UIScreen.main.bounds.height * 0.3 {
                dismissBottomSheet()
            } else {
                // 回弹到显示状态
                bottomConstraint.constant = 0
                UIView.animate(withDuration: 0.2) {
                    self.view.layoutIfNeeded()
                }
            }
        }
    }
}

// 避免点击内容容器时触发背景的tap手势
extension BottomSheetContainerViewController: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        return touch.view == view
    }
}

步骤2:在Root ViewController中使用

Root VC里有四个按钮,点击时切换对应的内容VC并展示Bottom Sheet:

import UIKit

class RootViewController: UIViewController {
    // 保存Bottom Sheet容器的引用,实现复用
    var bottomSheetContainer: BottomSheetContainerViewController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setupActionButtons()
    }
    
    private func setupActionButtons() {
        // 创建按钮栈视图
        let buttonTitles = ["VC 1", "VC 2", "VC 3", "VC 4"]
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 16
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        
        // 布局栈视图到屏幕中心
        NSLayoutConstraint.activate([
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        
        // 添加按钮
        for title in buttonTitles {
            let button = UIButton(type: .system)
            button.setTitle(title, for: .normal)
            button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium)
            button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
            stackView.addArrangedSubview(button)
        }
    }
    
    @objc private func buttonTapped(_ sender: UIButton) {
        // 根据按钮标题创建对应的内容VC(替换成你自己的VC类)
        let contentVC: UIViewController
        switch sender.title(for: .normal) {
        case "VC 1":
            contentVC = FirstContentViewController()
        case "VC 2":
            contentVC = SecondContentViewController()
        case "VC 3":
            contentVC = ThirdContentViewController()
        case "VC 4":
            contentVC = FourthContentViewController()
        default:
            contentVC = UIViewController()
            contentVC.view.backgroundColor = .lightGray
        }
        
        // 复用已有容器,或者创建新的
        if let container = bottomSheetContainer {
            container.setContentViewController(contentVC)
            container.showSheet()
        } else {
            let container = BottomSheetContainerViewController()
            container.setContentViewController(contentVC)
            // 用overFullScreen模式,避免Root VC被隐藏
            container.modalPresentationStyle = .overFullScreen
            present(container, animated: false) {
                container.showSheet()
            }
            bottomSheetContainer = container
        }
    }
}

// 示例内容VC,替换成你自己的VC即可
class FirstContentViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .cyan
        let label = UILabel()
        label.text = "第一个内容页面"
        label.textAlignment = .center
        label.font = UIFont.systemFont(ofSize: 20, weight: .bold)
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

// 其他三个内容VC可以参考上面的写法,这里省略...

额外优化建议

  • 添加拖拽指示器:在内容容器顶部加一个小横线(UIView),让用户更明确知道可以拖拽
  • 自适应高度:根据内容VC的实际高度调整Sheet的高度,而不是固定60%
  • 背景动画:显示时添加背景从透明到半透明的渐变动画,关闭时反向渐变
  • 旋转适配:在viewWillTransition(to:with:)方法中更新约束,避免旋转后布局错乱
  • 支持多种高度:比如添加方法让外部指定Sheet的高度(比如全屏、半屏、自定义高度)

内容的提问来源于stack exchange,提问作者Ben Dev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 06:43:43