UIVisualEffectView with mask doesn’t blur in iOS 10

I’ve got a UIVisualEffectView that I mask with a path. This works fine in iOS 9 (and I think iOS 8, though I don’t support that any more).


In iOS 10, there is no actual blur effect (though the masking still happens). There’s just an alpha background. Is this a known change?


My initializer does


CAShapeLayer* mask = [CAShapeLayer layer];
mask.path = path.CGPath;
self.layer.mask = mask;

Replies

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    view.backgroundColor = .clear

    let shapeLayer = CAShapeLayer()
    let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: [.topRight, .topLeft], cornerRadii: CGSize(width: 10, height: 10))
    shapeLayer.path = path.cgPath

    let mask = UIView(frame: view.bounds)
    mask.backgroundColor = .clear
    let areaToReveal = UIView(frame: view.bounds)
    areaToReveal.layer.mask = shapeLayer
    areaToReveal.backgroundColor = .white
    mask.addSubview(areaToReveal)
    view.mask = mask
}

func setupViews() {
    view.addSubview(blurredView)
}

func addStaticConstraints() {
    blurredView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
    blurredView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    blurredView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
    blurredView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}


Above is what I've tried. Blur shows in iOS 11, but not in iOS 10. I've also tried adding the mask to the `blurredView` directly on line 16 to no avail.

On iOS 10 blurs can't sample content "through" a mask – because you make the visual effect view a subview of a view with a mask you prevent it from sampling the content it needs to do the masking.


You need to apply the mask *directly* to the UIVisualEffectView, not to a superview. This works on iOS 11 due to improvements specifially made on that OS version.

See my edit above. Is that the approach you're describing?


"I've also tried adding the mask to the `blurredView` directly on line 16 to no avail."

I would recommend process of elimination then – try a simpler mask first and see if that resolves the issue (such as just setting corner radius on a plain view instead of going through the shape layer business).


However, fundamentally the best way to create a mask view that uses a shape layer is to create your own UIView subclass that overrides .layerClass() to return CAShapeLayer.self. That removes the extra 2 views/layers in your example and simplifies greatly by not using a mask of a subview of a view mean to be a mask.

let mask = UIView(frame: blurredView.bounds)
mask.backgroundColor = .clear
let areaToReveal = UIView(frame: blurredView.bounds)
areaToReveal.layer.cornerRadius = 10.0
areaToReveal.backgroundColor = .black
mask.addSubview(areaToReveal)
blurredView.mask = mask


Not sure if this is what you meant; but it still doesnt work.

class PassthroughView: UIView {
   
    override class var layerClass: AnyClass {
        get {
            return CAShapeLayer.self
        }
    }
    private var corners = UIRectCorner.allCorners
    private var radius: CGFloat = 10.0
   
    required init(frame: CGRect, corners: UIRectCorner = .allCorners, radius: CGFloat = 10.0) {
        super.init(frame: frame)
        self.corners = corners
        self.radius = radius
        backgroundColor = .clear
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
   
    override func layoutSubviews() {
        super.layoutSubviews()
       
        guard let layer = layer as? CAShapeLayer else {
            return
        }
       
        let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        layer.path = path.cgPath
    }
}


MyViewController: UIViewController {
  override func viewDidLayoutSubviews() {
      super.viewDidLayoutSubviews()
      blurredView.mask = PassthroughView(frame: blurredView.bounds,
                                       corners: [.topLeft, .topRight],
                                        radius: 10.0)
  }
  
  func setupViews() {
      view.addSubview(blurredView)
  }
}



This works on iOS 11, but not on iOS 10. Seems to be the common denominator with regards to rounding/masking top corners of a UIVisualEffectView. Not sure what else to try.

@Rincewind, here's my summary of findings:


There seems to be an issue in iOS 10 that causes the UIView.mask property to not work when set on UIVisualEffectView when done so in viewWillLayoutSubviews(). When done in viewDidLoad(), works fine (But wont work if views are setup with Auto Layout). That same code works on UIView, with and without auto layout in iOS 10 in viewWillLayoutSubviews(). The same code also works in iOS 11 for both UIView and UIVisualEffectView in viewWillLayoutSubviews().


I'm not sure where to go from here to try to get top rounded corners for a UIVisualEffectView on iOS 10. Any help is much appreciated.


class PassthroughView: UIView {

    override class var layerClass: AnyClass {
        get {
            return CAShapeLayer.self
        }
    }
    private var corners = UIRectCorner.allCorners
    private var radius: CGFloat = 10.0

    required init(frame: CGRect, corners: UIRectCorner = .allCorners, radius: CGFloat = 10.0) {
        super.init(frame: frame)
        self.corners = corners
        self.radius = radius
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        guard let layer = layer as? CAShapeLayer else {
            return
        }

        layer.fillColor = UIColor.black.cgColor

        let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        layer.path = path.cgPath
    }
}


class ViewController: UIViewController {
    @IBOutlet weak var myView: UIView!
    private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))

    override func viewDidLoad() {
        super.viewDidLoad()
   
        view.addSubview(blurView)
        blurView.frame = CGRect(x: 16, y: 328, width: 337, height: 218)
   
        / / This works for UIVisualEffectView in iOS 10, but fails if views are setup via Auto Layout
        let passView = PassthroughView(frame: blurView.bounds, corners: [.topRight, .topLeft], radius: 10.0)
        blurView.mask = passView
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        / / This causes UIVisualEffectView to not be shown in iOS 10.
        let passView = PassthroughView(frame: blurView.bounds, corners: [.topRight, .topLeft], radius: 10.0)
        blurView.mask = passView
   
        / / This works
        let passView2 = PassthroughView(frame: myView.bounds, corners: [.topRight, .topLeft], radius: 10.0)
        myView.mask = passView2
    }
}