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

如何在UIVisualEffectView中实现透明镂空?效果反转问题求解

UIVisualEffectView 透明镂空效果反转问题解决方案

问题复现

实现了通过overCurrentContext模态样式弹出的UIViewController,页面添加UIVisualEffectView实现暗模糊效果,初始展示正常。尝试通过CAShapeLayer配合evenOdd填充规则给模糊视图添加指定矩形的透明镂空,初始实现代码如下:

class CoverView: UIView {
    private let blurView: UIVisualEffectView = {
        UIVisualEffectView(effect: UIBlurEffect(style: .dark))
    }()

    // MARK: - Internal

    func setup() {
        addSubview(blurView)
        blurView.snp.makeConstraints { maker in
            maker.edges.equalToSuperview()
        }
        blurView.makeClearHole(rect: CGRect(x: 100, y: 100, width: 100, height: 230))
    }
}

extension UIView {
    func makeClearHole(rect: CGRect) {
        let maskLayer = CAShapeLayer()
        maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
        maskLayer.fillColor = UIColor.black.cgColor

        let pathToOverlay = UIBezierPath(rect: bounds)
        pathToOverlay.append(UIBezierPath(rect: rect))
        pathToOverlay.usesEvenOddFillRule = true
        maskLayer.path = pathToOverlay.cgPath

        layer.mask = maskLayer
    }
}

运行后效果与预期完全反转:预期效果为指定矩形区域透明、其余区域保持模糊;实际效果为指定矩形区域显示模糊,其余区域透明。
后续调整镂空实现代码,直接操作blurView的layer.mask,效果仍然没有改善:

private func makeClearHole(rect: CGRect) {
    let maskLayer = CAShapeLayer()
    maskLayer.fillColor = UIColor.black.cgColor

    let pathToOverlay = CGMutablePath()
    pathToOverlay.addRect(blurView.bounds)
    pathToOverlay.addRect(rect)
    maskLayer.path = pathToOverlay
    maskLayer.fillRule = .evenOdd
    blurView.layer.mask = maskLayer
}

问题根源

效果反转的核心原因是调用镂空逻辑的时机错误:

  • 代码在setup()方法中刚给blurView添加完约束就执行镂空计算,此时Auto Layout还未完成布局流程,blurView.bounds的值为默认的CGRect.zero,并非视图最终显示的实际尺寸
  • evenOdd填充规则下,路径重叠次数为奇数的区域会被判定为不透明(显示模糊效果),重叠次数为偶数的区域判定为透明。当外框路径是0尺寸矩形时,只有传入的镂空矩形区域被判定为不透明,其余区域全部透明,刚好和预期完全相反
  • 初始生成mask路径后,没有监听视图布局变化更新路径,即使后续视图完成布局,mask仍然保持初始错误状态

正确实现方案

核心是等视图完成布局拿到真实bounds后再生成mask,同时在布局变化时自动更新mask路径,代码如下:

class CoverView: UIView {
    private let blurView: UIVisualEffectView = {
        UIVisualEffectView(effect: UIBlurEffect(style: .dark))
    }()
    // 保存镂空矩形配置
    private var targetHoleRect: CGRect = .zero

    override func layoutSubviews() {
        super.layoutSubviews()
        // 布局完成拿到真实bounds后更新mask
        updateBlurViewHoleMask()
    }

    func setup(holeRect: CGRect) {
        targetHoleRect = holeRect
        addSubview(blurView)
        blurView.snp.makeConstraints { maker in
            maker.edges.equalToSuperview()
        }
    }

    private func updateBlurViewHoleMask() {
        // 尺寸无效时不生成mask
        guard blurView.bounds.size != .zero else { return }
        let maskLayer = CAShapeLayer()
        maskLayer.fillRule = .evenOdd
        maskLayer.fillColor = UIColor.black.cgColor

        let maskPath = CGMutablePath()
        maskPath.addRect(blurView.bounds)
        maskPath.addRect(targetHoleRect)
        maskLayer.path = maskPath
        blurView.layer.mask = maskLayer
    }

    // 提供动态更新镂空区域的方法
    func updateHoleRect(_ newRect: CGRect) {
        targetHoleRect = newRect
        setNeedsLayout()
    }
}

注意事项

  • 不要在init、viewDidLoad、刚添加完约束的节点调用需要依赖视图bounds的绘制逻辑,此时视图尚未完成布局,拿到的尺寸不是最终显示尺寸
  • 如果CoverView是在控制器中使用,若需要适配屏幕旋转、尺寸类变化,上述layoutSubviews的更新逻辑会自动触发,不需要额外处理
  • 不要将mask添加到UIVisualEffectView的contentView上,直接给blurView本身的layer设置mask即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 02:36:50