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

>Is this a known change?


For that, check the release notes and diffs.

I'm seeing the same issue - I've filed radar 27189321

OK, Radar 27227280 for me.

Seeing the same, Radar is 27393759

Same here 27467837

Still an issue in beta 3, updated Radar 27393759with a sample project

import UIKit
class BlurBacking: UIView {
    var blurView : UIVisualEffectView!
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.blueColor()
        let colorView = UIView(frame: CGRect(origin: CGPoint(x:20.0, y:20.0), size: CGSize(width:120.0, height:120.0)))
        colorView.backgroundColor = UIColor.redColor()
        self.addSubview(colorView)
        let blur = UIBlurEffect(style: .Light)
        let blurView = UIVisualEffectView(effect: blur)
        blurView.frame = CGRect(origin: CGPoint(x:40.0, y:40.0), size: CGSize(width:240.0, height:240.0))
        self.addSubview(blurView)
        self.blurView = blurView
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

//UIVisual effects view behaves as expected without masking a layer
        let unmaskedBlur = BlurBacking(frame:CGRect(origin: CGPoint(x:0.0, y:0.0), size: CGSize(width:view.frame.size.width, height:view.frame.size.height * 0.5)))
        view.addSubview(unmaskedBlur)

// Adding a mask layer to the UIVisualEffects view breaks the blur
        let maskedBlur = BlurBacking(frame:CGRect(origin: CGPoint(x:0.0, y:view.frame.size.height * 0.5), size: CGSize(width:view.frame.size.width, height:view.frame.size.height * 0.5)))
        view.addSubview(maskedBlur)
        let maskLayer = CAShapeLayer()
        maskLayer.frame = CGRect(origin: CGPoint(x:10.0, y:10.0), size: CGSize(width:140.0, height:140.0))
        maskLayer.path = UIBezierPath(ovalInRect: maskLayer.bounds).CGPath
        maskedBlur.blurView.layer.mask = maskLayer
    }
}

I received a message from my Radar 27393759 that masking the layer property of a UIVisualEffectsView will not work, and was encouraged to use the maskView property on the UIVisualEffects view. However, this does not work for me on either iOS 9 or 10. I've updated the Radar and sent them an updated sample project showing how the suggested technique does not work. Will update with more info when I hear more.


Quoted from Radar:

Masking the layer of a visual effect view is not guaranteed to produce the correct results – in some cases on iOS 9 it would produce an effect that looked correct, but potentially sourced the wrong content. Visual effect view will no longer source the incorrect content, but the only supported way to mask the view is to either use cornerRadius directly on the visual effect view’s layer (which should produce the same result as you are attempting here) or to use the visual effect view’s maskView property.


My updated sample code:

import UIKit
class BlurBacking: UIView {
    var blurView : UIVisualEffectView!
    var regularView : UIView!
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.blueColor()
        let colorView = UIView(frame: CGRect(origin: CGPoint(x:20.0, y:80.0), size: CGSize(width:280.0, height:80.0)))
        colorView.backgroundColor = UIColor.redColor()
        self.addSubview(colorView)

        let blur = UIBlurEffect(style: .Light)
        let blurView = UIVisualEffectView(effect: blur)
        blurView.frame = CGRect(origin: CGPoint(x:40.0, y:40.0), size: CGSize(width:80.0, height:80.0))
        self.addSubview(blurView)
        self.blurView = blurView

        let normalView = UIView(frame: CGRect(origin: CGPoint(x:140.0, y:40.0), size: CGSize(width:80.0, height:80.0)))
        normalView.backgroundColor = UIColor.greenColor()
        self.addSubview(normalView)
        regularView = normalView
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class ViewController: UIViewController {
    func maskFrame() -> CGRect {
        return CGRect(origin: CGPoint(x:10.0, y:10.0), size: CGSize(width:60.0, height:60.0))
    }

    func maskView() -> UIView {
        let maskView = UIView(frame: maskFrame())
        maskView.backgroundColor = UIColor.blackColor()
        maskView.layer.cornerRadius = 8.0
        return maskView
    }

    func maskLayer() -> CALayer {
        let maskLayer = CAShapeLayer()
        maskLayer.frame = maskFrame()
        maskLayer.path = UIBezierPath(roundedRect: maskLayer.bounds, cornerRadius: 8.0).CGPath
        return maskLayer
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        //UIVisual effects view behaves as expected without masking a layer
        let unmaskedBlur = BlurBacking(frame:CGRect(origin: CGPoint(x:0.0, y:0.0), size: CGSize(width:view.frame.size.width, height:view.frame.size.height * (1.0/3.0))))
        view.addSubview(unmaskedBlur)

        // Adding a mask layer to the UIVisualEffects view breaks the blur, works with UIView
        let maskedBlur = BlurBacking(frame:CGRect(origin: CGPoint(x:0.0, y:view.frame.size.height * (1.0/3.0)), size: CGSize(width:view.frame.size.width, height:view.frame.size.height * (1.0/3.0))))
        view.addSubview(maskedBlur)
        maskedBlur.blurView.layer.mask = maskLayer()
        maskedBlur.regularView.layer.mask = maskLayer()

        //Use View to mask a UIVisualEffectsView and a UIView
        let maskViewBlur = BlurBacking(frame: CGRect(origin: CGPoint(x:0.0, y:view.frame.size.height * ((1.0/3.0) * 2.0)), size: CGSize(width:view.frame.size.width, height:view.frame.size.height * (1.0/3.0))))
        view.addSubview(maskViewBlur)
        maskViewBlur.blurView.maskView = maskView()
        maskViewBlur.regularView.maskView = maskView()
    }
}

Try setting the mask after -viewDidLoad (such as in -viewWillLayoutSubviews). If that works, then the likely issue was an additional bug we fixed after beta3.


Masking the visual effect view's layer still ends up breaking the effect, but we found another issue where sometimes we didn't capture the mask properly in the first place.


That said, if you don't mind using more tranditional rounded corners, just setting cornerRadius=8 and clipsToBounds=YES would probably do what you want without nearly the complexity here.

Going to look into this but corner radius is not the issue in 27227280, my mask is a cartoon text balloon.

I'm not certain what you were told, but the root issue isn't anything like corner radius, but rather how visual effect view works, and how masking impacts that functionality.


UIVisualEffectView works by capturing the content behind it, and then applying filters (such as blur) to that content. This is important to understand because when visual effect view goes to do that capture, it can only capture what is in the current render buffer. But when you do various tasks that require an offscreen pass (such as masking) that creates a new render buffer that then causes the capture phase to capture less content – only what is in that current render buffer.


So if you apply a mask to a UIVisualEffectView via the CALayer.mask property, you end up putting the whole visual effect view into an offscreen pass, which means it cannot capture the content it needs to render. If you use the UIView.maskView UIVisualEffectView works around this by taking a snapshot of the mask instead, and applying it in the correct places to ensure that the capture can still do the correct thing.


One bug that we fixed recently had to do with snapshots of that maskView not working – that bug exists in Beta3.


That is why it is important to use the UIView.maskView property when working with a UIVisualEffectView instead of the CALayer.mask property, as otherwise the effect will be broken. Other bugs allowed this to work prior to iOS 10 in limited circumstances, but usually by causing incorrect rendering.


Now corner radius came up with one of the reported issues because the developer was basically just using a rounded rectangle mask – cornerRadius+clipsToBounds does not have the issue that general masking does, so that would work without any effort. For most of your masking needs, I suspect the advice is irrelevant.


I would also highly recommend that anyone with questions on broken visual effects watch the 2015 WWDC session on Whats New in UIKit Dynamics and Visual Effects, where we discuss all the ways your effect can break due to offscreen passes.

Your issue is also using CALayer.mask, using UIView.maskView would probably work better for your needs (modulo bugs that exist in Beta3 at the moment).


For all concerned, the fastest way to test if you don't have the bug in Beta3 that prevents UIVisualEffectView's snapshotting from working would be to set your maskView inside of -[UIView layoutSubviews] or -[UIViewController viewWillLayoutSubiews].

Oh, and I just noticed that Beta4 should be out now – if you were having issues with UIView.maskView, give that a try there.

Thanks Rincewind, in my sample, I used a simple shape, but i actually need to be able to mask an arbitrary path, so that's why I included creating a CAShapeLayer with a path.


I've confirmed that beta 4 resolves my issue in setting the maskView property on the UIVisualEffectView. I'm able to create the arbitrary mask by:

  1. creating a CAShapeLayer
  2. creating a UIBezierPath and set the layer's path property with it
  3. creating a UIView
  4. setting the mask property of the view's layer property to my mask layer
  5. then setting the UIVisualEffectView's maskView property to the mask view



    UIBlurEffect *blur = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    UIVisualEffectView *visualEffect = [[UIVisualEffectView alloc] initWithEffect:blur];
    visualEffect.frame = self.view.bounds;

    /// 1.
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = view.bounds;
   
    /// 2.
    UIBezierPath *bezier = [UIBezierPath bezierPathWithOvalInRect:someRect];
    maskLayer.path = bezier.CGPath;

    ///3.
    UIView *maskView = [[UIView alloc] initWithFrame:self.view.bounds];
    maskView.backgroundColor = [UIColor blackColor];

    /// 4.
    maskView.layer.mask = maskLayer;

    /// 5.
    visualEffect.maskView = maskView;




I've noticed that the Prominent and Regular effects are broken with and without masking, but will report in another bug when I get time.


I've marked the masking bug Radar 27393759 as being resolved.


Thanks for your help!

I would avoid using 2-level masking like that. For one it isn't guaranteed to work (see the comments on CALayer.mask), as nested masking isn't guaranteed by Core Animation. But even if it does work it is less efficient, especially in a case like this.


Instead you can create a simple UIView subclass that uses a CAShapeLayer as its backing layer by overriding +layerClass.


Also keep in mind that masking only cares about the alpha coverage. In this case, seeing the fillColor of the CAShapeLayer would be more efficient than using the CAShapeLayer as a mask of a view with a background color. And even then, only the alpha value of that fillColor matters.

David, if I can ask a dumb question: how do you use a maskView? I didn't have any success when I tried (nothing at all showed). Does the view need to be inserted into any hierarchy? Is its frame supposed to be the same as the UIVisualEffectView, or is it at 0,0? I find the documentation unclear, and no sample code.


I'm guessing that in my case I can (in theory) have a clear view, and draw a text balloon into it, then use it as maskView.