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

iOS开发:如何使presentableView外部的dismissButton可点击?

问题描述

我创建了一个自定义UIPresentationController,希望屏幕顶部1/6区域包含dismissButton和dimmingView,分别在弹窗展示和消失时淡入淡出。但由于dismissButton位于presentedView外部,无法被点击。请问有解决方案吗?

注:我知道可以将子视图添加到presentedView后再把dismissButton移到视图外,但这样按钮会和控制器其他部分一样以向上滑动的方式弹出,而非我想要的淡入效果。

相关代码

@available(iOS 13.0, *)
open class CUICFiveSixthPopUpTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
    
    let dimmingColor: UIColor!
    
    public init(dimmingColor: UIColor) {
        self.dimmingColor = dimmingColor
        super.init()
    }
    
    public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return CUICFiveSixthPopUpPresentationController(presentedViewController: presented, presenting: presenting, dimmingColor: dimmingColor)
    }
}

@available(iOS 13.0, *)
open class CUICFiveSixthPopUpPresentationController: UIPresentationController {
    open override var frameOfPresentedViewInContainerView: CGRect {
        let bounds = presentingViewController.view.bounds
        
        let size = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height*5/6)
        let origin = CGPoint(x: bounds.midX - size.width / 2, y: UIScreen.main.bounds.height - size.height)
        return CGRect(origin: origin, size: size)
    }
    
    public init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, dimmingColor: UIColor) {
        super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
        
        presentedView?.autoresizingMask = [
            .flexibleTopMargin,
            .flexibleBottomMargin,
            .flexibleLeftMargin,
            .flexibleRightMargin
        ]
        
        presentedViewController.view.layer.cornerRadius = viewRadius
        presentedViewController.view.clipsToBounds = true
        dimmingView.backgroundColor = dimmingColor
        dismissButton.backgroundColor = dimmingColor.lighter()
        
        presentedView?.translatesAutoresizingMaskIntoConstraints = true
    }
    
    let iconSize: CGFloat = 40
    let viewRadius: CGFloat = 35
    
    let dimmingView: UIView = {
        let dimmingView = UIView(frame: .zero)
        dimmingView.translatesAutoresizingMaskIntoConstraints = false
        return dimmingView
    }()
    
    let dismissButton: UIButton = {
        let dismissButton = UIButton(frame: .zero)
        dismissButton.tintColor = .white
        dismissButton.alpha = 0.8
        dismissButton.setImage(.close, for: .normal)
        dismissButton.translatesAutoresizingMaskIntoConstraints = false
        return dismissButton
    }()
    
    @objc private func dimmingViewTapped(_ sender: UITapGestureRecognizer) {
        let point = sender.location(in: presentingViewController.view!)
        if dismissButton.frame.contains(point) {
            presentedViewController.dismiss(animated: true)
        }
    }
    
    open override func presentationTransitionWillBegin() {
        super.presentationTransitionWillBegin()
        
        let superview = presentingViewController.view!
        let presentedView = presentedView!
        
        superview.addSubview(dimmingView)
        superview.addSubview(dismissButton)
        
        NSLayoutConstraint.activate([
            dimmingView.leadingAnchor.constraint(equalTo: superview.leadingAnchor),
            dimmingView.trailingAnchor.constraint(equalTo: superview.trailingAnchor),
            dimmingView.bottomAnchor.constraint(equalTo: superview.bottomAnchor),
            dimmingView.topAnchor.constraint(equalTo: superview.topAnchor),
            
            dismissButton.topAnchor.constraint(equalTo: superview.topAnchor, constant: .marginFromTopOfScreen),
            dismissButton.leftAnchor.constraint(equalTo: superview.leftAnchor, constant: .marginLeft),
            dismissButton.heightAnchor.constraint(equalToConstant: iconSize),
            dismissButton.widthAnchor.constraint(equalToConstant: iconSize),
        ])
        dismissButton.layer.cornerRadius = iconSize/2
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dimmingViewTapped(_:)))
        dimmingView.addGestureRecognizer(tapGesture)
        
        dimmingView.alpha = 0
        presentingViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] _ in
            self?.dimmingView.alpha = 1
        }, completion: nil)
    }
    
    open override func dismissalTransitionWillBegin() {
        super.dismissalTransitionWillBegin()
        
        presentingViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
            self.dimmingView.alpha = 0
        }, completion: { _ in
            self.dimmingView.removeFromSuperview()
        })
    }
}
解决方案

核心问题是你把dismissButton添加到了presentingViewController.view上,弹窗展示时presentedViewController.view的窗口层级更高,拦截了点击事件。这里提供两个可行方案:

方案一:将按钮和蒙层添加到容器视图(Container View)

UIPresentationController自带的containerView是专门承载弹窗相关视图的容器,层级关系更合理,推荐使用这个方案:

  1. 修改presentationTransitionWillBegin方法,将视图添加到containerView而非原控制器视图:
open override func presentationTransitionWillBegin() {
    super.presentationTransitionWillBegin()
    
    guard let containerView = containerView else { return }
    
    // 把视图添加到containerView
    containerView.addSubview(dimmingView)
    containerView.addSubview(dismissButton)
    
    // 约束目标换成containerView
    NSLayoutConstraint.activate([
        dimmingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
        dimmingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
        dimmingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
        dimmingView.topAnchor.constraint(equalTo: containerView.topAnchor),
        
        dismissButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: .marginFromTopOfScreen),
        dismissButton.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: .marginLeft),
        dismissButton.heightAnchor.constraint(equalToConstant: iconSize),
        dismissButton.widthAnchor.constraint(equalToConstant: iconSize),
    ])
    
    dismissButton.layer.cornerRadius = iconSize/2
    // 给按钮直接添加点击事件,替代原来的蒙层手势判断
    dismissButton.addTarget(self, action: #selector(dismissButtonTapped), for: .touchUpInside)
    
    dimmingView.alpha = 0
    dismissButton.alpha = 0
    presentingViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] _ in
        self?.dimmingView.alpha = 1
        // 同时给按钮添加淡入动画
        self?.dismissButton.alpha = 1
    }, completion: nil)
}
  1. 添加按钮点击的响应方法:
@objc private func dismissButtonTapped() {
    presentedViewController.dismiss(animated: true)
}
  1. 调整消失动画,给按钮也加上淡出效果:
open override func dismissalTransitionWillBegin() {
    super.dismissalTransitionWillBegin()
    
    presentingViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] _ in
        self?.dimmingView.alpha = 0
        self?.dismissButton.alpha = 0
    }, completion: { [weak self] _ in
        self?.dimmingView.removeFromSuperview()
        self?.dismissButton.removeFromSuperview()
    })
}

这样按钮和蒙层都在containerView中管理,层级不会被弹窗遮挡,能正常响应点击,同时淡入淡出的动画效果也能独立实现。

方案二:手动提升按钮层级

如果坚持要把按钮放在原控制器视图上,可以在弹窗展示完成后将按钮提到最顶层:

open override func presentationTransitionDidEnd(_ completed: Bool) {
    super.presentationTransitionDidEnd(completed)
    if completed {
        presentingViewController.view.bringSubviewToFront(dismissButton)
    }
}

但这种方式容易造成视图层级混乱,不如方案一符合UIPresentationController的设计规范,不推荐优先使用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 02:50:00