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;

>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.

The maskView is in the coordinate system of the visual effect view (i.e. maskView.frame = visualEffectView.bounds is typical).


The alpha channel of the composited maskView is used to blend the content of the view that is masked. So for example if you had a horizontal alpha gradient from left to right of alpha=0 to alpha=1, then you would see progressively more view from left to right.


Note that the actual color (R/G/B) information doesn't matter, just the alpha.


Now, the fact that the mask's frame is typically the same as the view's bounds is only a typical case – you can actually use other values if you like and use non-zero origins. That just means that you are going to be selecting parts of the view rather than the whole view. While not common, this can be useful for various effects.


But in the general case UIView.maskView and CALayer.mask should operate fairly similarly. The exception is UIVisualEffectView, where due to its nature we need to snapshot the mask and apply it over multiple subviews. This can be problematic in cases such as when using auto layout as you need to recalculate the mask during layout (due to the hierarchy requirements a mask can't be in the layer tree, so it won't laid out by auto layout or springs & struts).

Hello,


i have the following view hierarchies:


1)

UIView (clearColor background)

|---UIView (clearColor background)

|--- BlurView (inherits UIVisualEffectsView)


2)

UIView (clearColor background)

|--- BlurView (inherits UIVisualEffectsView)


3)

UIViewController

|---MKMapView

|--- .... some other views

|---UIView == 1)

|---UIView == 2)


The code if the blurView is as follows:


class BlurView: UIVisualEffectView {
    var cornersToRound = UIRectCorner.AllCorners
    private override init(effect: UIVisualEffect?) {
        super.init(effect: effect)
    }
    convenience init(effect: UIVisualEffect?, corners: UIRectCorner) {
        self.init(effect: effect)
        cornersToRound = corners
        translatesAutoresizingMaskIntoConstraints = false
    }
   
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        let radius = kDefaultCornerRadius
        let path = UIBezierPath(roundedRect:bounds, byRoundingCorners:cornersToRound, cornerRadii: CGSizeMake(radius, radius))

        let shapeLayer = CAShapeLayer()
        let maskView = UIView(frame: bounds)
        shapeLayer.path = path.CGPath
        maskView.layer.mask = shapeLayer
        maskView.backgroundColor = UIColor.blackColor()
        self.maskView = maskView
    }
}


In the first view hierarchy I don't get the blur plus the rounded corners but in the second one it works. The main differences are

a) Hierarchy 2) is added as reaction on a user button press when all views are already set up. Hierarchy 1) is added and shown in viewDidLoad of the viewController as shown in 3).

b) The blurView in hierarchy 1) must sample through 2 clear colored views. In hierarchy 2) it must sample through only 1 view.

c) In hierarchy 1) the rounded corners are only top left and top right. In hierarchy 2) all corners are rounded.


The effect appears only in iOS10 using either Xcodes 7 or 8. In iOS 9 it looks as expected (rounded corners + blurred background).

Thanks Rincewind, I'll refactor my code based on your advice.


To clarify is this what you mean when you advise to "see the comments on CALayer.mask"?:

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.


I appreciate the complexities of rending something like the blur effect and appreciate you taking the time to tell us the right way to work with them while masking, but documentation like this buried deep in the forums is only helpful to a few. Even if this were documented somewhere, it's complicated. I feel like I need to know an awful lot about the inner workings of UIVisualEffect views in order to mask them.


I'd just like to pose a rhetorical question: Could this be simpler?


Again, thanks for the help and I appreciate the responsiveness.

My primary concern is that If you setup something like this:


CALayer *layer1 = ..., *mask1 = ..., *mask2 = ...;
mask1.mask = mask2;
layer1.mask = mask1;


Core Animation specifically states that this is an unsupported situation. I haven't tried it, so I can't tell you if it "works" or not right now, but that it works with UIVisualEffectView would be a side effect of its implementation, and is an implementation detail that we would very much like to eliminate if at all possible someday (i.e. make this simpler!).


The differences you are seeing between iOS 9 and iOS 10 are unfortunately along the same lines of the above - something "worked" and now it doesn't (it never really worked reliably, but it worked well enough to not notice the difference). And we could have sent a stronger message on what the correct things to do here are. If nothing else, maybe we'll have more talks on visual effects at WWDC 🙂.

Given what you've said, there shouldn't be any difference between #1 and #2. The number of views to "sample through" isn't generally relevant, unless one of those views cause an offscreen pass (such as by setting alpha on that view). Similarly the number of corners rounded shouldn't matter, since that is captured in the mask (but again, see my comment above about this pattern for masking – is it not guaranteed to work by Core Animation and thus may be broken in the future).

blackjacx, I have tested with a code very similar to the one you provided and obtained similar results (blur not displaying when maskView is used). But I have also found that forcing a layout on the view after it is added to the hierarchy makes it to display as expected:


        let test = OverlayPanel()
        test.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
        test.cornersToRound = .allCorners
        view.addSubview(test)
        test.setNeedsLayout()
        test.layoutIfNeeded()


My testing is fairly limited and, as this issue seems inconsistent, I don't know if it will help in your case; but I wanted to let you know in case you find it useful.

Hi Rincewind,


Thank you for your explanations.

I'm doing all things correctly as you said. But the UIVisualEffectView is not visible.


Here's my code.


//UIView subclass - CAShapeLayer is the backing layer.
class ShapeLayerView: UIView
{
    let shapeLayer = CAShapeLayer()

    override var layer: CALayer
    {
        return shapeLayer
    }
    init(path: UIBezierPath)
    {
        super.init(frame: .zero)

//Setting path and fillColor for the shapeLayer
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.path = path.cgPath
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

//UITableViewCell subclass - has blurView
class BluredCell: UITableViewCell
{
    override func layoutSubviews()
    {
        super.layoutSubviews()
   
        let cornerRadii = CGSize(width: 10, height: 10)
     
        let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: cornerRadii)
        let view = ShapeLayerView(path: path)
        view.frame = self.bounds
//Masking the blurView
        self.blurView.mask = view
    }
}



Am I missing something?

Don't (in fact *never*) override UIView.layer, override UIView.layerClass. Basically you've created a layer that only you are using, not UIKit, hence the mask is empty, masking out everything.

I would like to confirm that this is indeed broken in iOS 10 GM and none of the proposed solutions in this thread work.

What in particular is broken for you?

UIVisualEffectView with mask doesn’t blur in iOS 10
 
 
Q