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

In this case it is the comment on CALayer.mask:


/* A layer whose alpha channel is used as a mask to select between the

* layer's background and the result of compositing the layer's

* contents with its filtered background. Defaults to nil. When used as

* a mask the layer's `compositingFilter' and `backgroundFilters'

* properties are ignored. When setting the mask to a new layer, the

* new layer must have a nil superlayer, otherwise the behavior is

* undefined. Nested masks (mask layers with their own masks) are

* unsupported. */


In the case of UIVisualEffectView today this ends up being irrelevant because UIVisualEffectView snapshots your mask view – thus doing essentially what you propose. However if in the future UIVisualEffectView stops snapshotting and uses your mask live, then you will fall into this undefined behavior from Core Animation, where UIVisualEffectView uses your view as a mask, and then CoreAnimation determines what it decides to do when rendering that mask.


So my comment is a warning to not use unsupported behavior, as you may end up with avoidable breakage.

Honestly guys, you really boneheaded this thing up.


So, your buggy broken version that lasted through a major release end to end works, for me, for OP, for other people, really nicely. You decide you need to "fix" it and by fixing it it means you completely broke it for all of us. Now, in order to understand what we need to do to fix what you broke when you broke what you fixed on something that was working fine for 99.9% of people, we have to dig through 37 comments and multiple websites and nary a straight forward comment.


The last time I touched this chunk of my code was 2 years ago. You are living and breathing this every day. When you read things like in SQLite they point out the importance of not going around smashing things because religion tells you to smash them because you have millions of users that are depending on the functionality is it is. You need to find a way to fork away from the stuff that for whatever reason you decided is not right.


Because for us, it was right. We had results that we worked at and we put away. Now you make us drag all this old stuff out again to try to slap some patches and redesign it. A just touch more courtesy to compatibility, even when you decide you need to fix things, is good. Simple instructions to warn us that this is coming is good. Simple instructions on how to deal with it is good.


Following the rules, finding something doesn't work suddenly in iOS10 and having to trawl the internet to find out why illustrates a broken contract between provider and users. If you are doing this here, and other teams are doing this elsewhere, with the massive complexity you have going on now PLUS two languages going side by side so half of us have to look at examples in a language that we're not using and try to port a solution to something that worked fine in the first place makes me want to delete my app.


I have a feeling that when I'm done, after 6 years of full time development you're just going to cut the cord on ObjC and tie my hands behind my back and shove me into the ocean with lead shoes on. This is the Apple approach to fixing problems. Always has been ever since NeXT nuked the Phone Kit because it had some bugs.


What you needed to do here was leave the existing path and flag it as being depricated and then develop a new path that didn't have the limitations the old one has. And then encourage people to move over. You deprecate constantly and change constantly and we need to book a serious portion of the year to trying to adapt to your ever shifting ideas. An app like mine that has spanned several different 270 degree turns from Apple ends up embracing all of the current trends in development that lasted a year or two and got superceded. I don't have the resources to rewrite 100k lines of code every time one of you guys has a bright idea. You need to focus a little more on stability of the development platform, it's just as important a concept os adding features.


I don't use Swift much but I have some and when your conversion tool to 3.0 broke all the swift and left in a bunch of manual things to change, it was amateur hour with a score of 10 out of 10. What ever happened to "it just works?" You give us these tools, you break them, your fixes break them and force us to redevelop. What is going on over there?

I spent some time with this and came up with nothing working... It seems to me that having a simple CAShapeLayer as the backing layer for a view won't work because the area that isn't part of the shape is rendered as black even if I set the layer and view's background color to clear.... so basically the shape will be ignored.


Here's my example. Make a new single view application project and add a single large jpg file called image.jpg to the project, then replace the contents of ViewController.m the code below. #define qJustShowMask to 1 to see the mask, and you'll see the black areas that should be the photo beneath... then to 0 to see the effect masked to the whole rect, ugh. This used to be a pretty easy thing to do, what am I missing here?!?


#import "ViewController.h"
#define qJustShowMask 1
@interface ShapeView : UIView
@end
@implementation ShapeView
+ (Class) layerClass
{
  return [CAShapeLayer class];
}
-(void)layoutSubviews
{
  [super layoutSubviews];
  [self updateMask];
}
-(void) updateMask
{
  self.backgroundColor = [UIColor clearColor];
  self.layer.backgroundColor = [UIColor clearColor].CGColor;
  UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds
    byRoundingCorners: UIRectCornerBottomRight |  UIRectCornerTopLeft
  cornerRadii:CGSizeMake(200, 200)];
  CAShapeLayer *maskLayer = (CAShapeLayer*) self.layer;
  maskLayer.fillColor = [UIColor cyanColor].CGColor;
  maskLayer.frame = self.bounds;
  maskLayer.path = maskPath.CGPath;
  [self setNeedsDisplay];
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
  [super viewDidLoad];
  UIImageView* backDrop = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"image.jpg"]];
  backDrop.frame = self.view.bounds;
  [self.view addSubview:backDrop];
  ShapeView* sv = [[ShapeView alloc] initWithFrame:backDrop.bounds];
#if qJustShowMask
  [self.view addSubview:sv];
#else
  UIVisualEffect* effect = [UIBlurEffect effectWithStyle: UIBlurEffectStyleLight];
  UIVisualEffectView* vfx = [[UIVisualEffectView alloc] initWithEffect:effect];
  vfx.frame = sv.frame;
  backDrop.maskView = sv;
  [self.view addSubview:vfx];
#endif
}
@end

Lines 17 & 18 are redundant – UIView.backgroundColor sets CALayer.backgroundColor.

Line 24 is just wrong – your maskLayer is the layer of 'self', and you are basically having the view (mask) reposition itself in the wrong coordinate system. Just delete this line.

Line 23 isn't wrong, but note that when used as a mask only the alpha value counts. But using say, cyanColor over blackColor isn't harmful.

Line 26 is also wrong – you do not want the view to display, as that will call -drawRect:, which you don't want with a shape layer. Its probably not actually calling out (because you don't implement -drawRect:) but it is semantically wrong given you are trying to use the shapelayer to provide your mask.

Line 43 is semantically backwards – you want the mask view's frame to be the visual effect view's bounds, not the other way around. This probably isn't an issue in your setup (because everything fully consumes the region of its superview) but it is conceptually wrong.

So, though inspite of some ineffective, semanitcally wrong but basically a nopp lines in this example, what is preventing the shape from masking the visual effect when qJustShowMask is 0?

The -setNeedsDisplayCall and the frame=bounds calls were the most likely to be at issue. Beyond that I'm not certain precisely what may have gone wrong – but my first stop would be to simplify (for example using a rect for the shape layer path, as well as the other typical debugging steps).


That said, you can also, at least for this case, avoid masking the visual effect all together by using 2 image views and an inverted mask. You place your image view and visual effect view as you already have, then you apply the inverted mask to another image view with the same image in the visual effect view's content view. You can invert the mask by setting the shape layer's fill mode to even-odd, and adding an additional rect with the bounds of the shape layer.

So for those interested in the output, I think I see what's going on.... and it's that we're talking about different expectations.


Apparently I can't post images here, that seems unfortunate, but I've posted it here https://s16.postimg.org/j9800la5x/masks.png


The UIVisualEffectView's frame is a little different than in the above exaple:

  ShapeView* sv = [[ShapeView alloc] initWithFrame:CGRectMake(0, self.view.bounds.size.height/4, self.view.bounds.size.width, self.view.bounds.size.height/2.0)];


The first image is the UIVisualEffectView without a mask view.


The second is the compiler switch to see the mask view.


The third is the UIVisualEffectView masked with the mask view. This result is that the source for the effect is masked, and I don't know what the use case is for that.


The fourth is the effect I was expecting to acheive and the effect that I think many others used to be able to aceive by applying a mask to the UIVisualEffectView or one of its superviews. What technique would you use to acheive that?

The 4th example is also what I would have expected, but I don't have an understanding of what is going wrong for you, so I can't really say what you should change.


You might want to talk to DTS if you need a timely response, or file a bug report if you just think this is our issue.

Hey @Rincewind,


Thanks for your continuous commitment to answering questions in this thread, I feel I can now get my head around how to do masking on UIVisualEffectView.


However, my problem is a bit more peculiar. I'm trying to build the folllowing UI design: https://dl.dropboxusercontent.com/spa/6d4j003yz96ozkz/smg3lrmp.png


The problem here is that the blurred view in the background is actually a video. So while the video plays, it will be blurred, and a small part of it will be visible while playing.


I tried to do this masking but couldn't achieve the desired result. Is it possible to do with just masking the UIVisualEffectView?


Or should I make the hierarchy like this:


AVPlayerLayer

UIVisualEffectView

AVPlayerLayer (with inverse masking?)


Any hints would be much appreciated, thanks!

Hi Rincewind (hope you're still listening),


(TL;DR: Read underlined question.)


I understand the description and subtleties of the offscreen rendering of UIVisualEffectViews, and I've implemented our effects to work in iOS 10 with your instructions here vis-à-vis the maskView property. However, this implementation is failing on iOS 9. Specifically, on iOS 9 the views are failing to render at all, as if they had their hidden property set to true.


This has 'nerd-sniped' a large portion of our iOS team here at Tulip, and we just can't figure it out. I've also recently watched the WWDC session "What's New in UIKit Dynamics and Visual Effects" and that hasn't helped me with this one. None of the posts here reference both iOS 9 and 10 and the same time, and so we're beginning to wonder if it's just not possible. Is it possible to have a single code-path implementation of a visual effect view that will work for both iOS 9 and iOS 10?


Our requirements include that only two corners be rounded, which means that we cannot simply use the cornerRadius & clipsToBounds method. Our requirements also include supporting both iOS 9 & 10 concurrently, and we hate divergent code paths as a solution to "well, this way used to work" problems.


Here is the pseudo code for our issue:

@implementation UIView (RoundedMask)
- (void)roundCorners:(UIRectCorner)corners withRadius:(CGFloat)radius {
    CGSize radii = CGSizeMake(radius, radius);

    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds
                                              byRoundingCorners:corners
                                                    cornerRadii:radii];

    CAShapeLayer *layer = [CAShapeLayer layer];    // Fill colour defaults to opaque black
    layer.path = path.CGPath;

    UIView *view = [[UIView alloc] initWithFrame:self.bounds];    // Background colour defaults to transparent.
    [view.layer addSublayer:layer]

    self.maskView = view;    // Masks self by removing pixels where maskView is transparent.
}
@end

(This method is sometimes used on non-effect views as well. We are aware of the "but what if your view resizes?" issue.)


If I break on line 15 and use the debugger QuickLook feature on the view variable, I do see a black opaque rectangle with the appropriate corners rounded and a transparent background. If I replace line 13 with the following three lines:

    view.backgroundColor = [UIColor blackColor];
    view.layer.cornerRadius = radius;
    view.clipsToBounds = YES;

… then the effect is properly masked and composited on both iOS versions, but of course fails our design requirement of partially-rounded rectangles. Previously, lines 12-14 were omitted, and line 15 read:

    self.layer.mask = layer;

… but of course this does not composite properly on iOS 10.


This method is invoked from a method that is invoked in a viewDidLoad method, but even if I move that invocation to the viewWillAppear: method as you recommend elsewhere (to get around a β bug) I get the same behaviour on iOS 9.


Also, you mentioned subclassing UIView to have a custom layerClass, and I thought perhaps there was a remote possibility that maskView does not support sublayers on iOS 9. So: I tried creating a subview, 'UIShapeView', with CAShapeLayer as its layerClass and with forwarding accessors for the path property. Changing line 12 to create one of these views and 13 to set its path (bypassing lines 9 & 10) was also futile.


When I use the View Hierarchy Debugger, and inspect _whatsWrongWithThisEffect of the non-visible view in iOS 9, I get the empty string. What's more, the content of the visual effect view is visible in that debugger, and all the way up and down the hierarchy (relative to the visual effect view) alpha is set to 1 and hidden is set to Off (clipsToBounds is set to Off all the way up the hierarchy (including the visual effect view itself), but subviews of the visual effect view do have clipsToBounds set to On).


The specific systems I'm getting these results from are both iPad minis running iOS 9.3.5 and iOS 10.2.1, and the app is built with Xcode 8.2.1 on macOS 10.12.3.


I am more that happy to investigate more on your prompting, or to send you screenshots via private message, but I think I've been as thorough as I possibly can. We'd sincerely appreciate any guidance you can give us on this issue, and we thank you for your time on this forum (this thread alone has been indispensable).

import UIKit
class BlurView: UIView{
    /
    /
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.clear

    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.backgroundColor = UIColor.clear
    }
    override func draw(_ rect: CGRect) {
    
        super.draw(rect)
   self.transparent()
    }


    func transparent()
    {  let path = UIBezierPath(rect: bounds)
        let tpath = UIBezierPath(rect: CGRect(x:60,y:120,width:200,height:200))
        path.append(tpath)
        path.usesEvenOddFillRule = true
    
    
        let layer = CAShapeLayer()
        layer.path = path.cgPath
        layer.fillRule = kCAFillRuleEvenOdd
        layer.fillColor = UIColor.black.cgColor
        self.layer.mask = layer
    
    }

}

i want a view with a transparent sqaure hole through which the image at the back can be seen and i want this view to be blurred. I created a custom class and this is how i use it.

let blurview = BlurView(frame: self.view.frame)

let  blur = UIVisualEffectView (effect: UIBlurEffect (style: UIBlurEffectStyle.dark))
        blur.frame = blurview.bounds

blurview.addSubview(blur)
        let imview = UIImageView(frame: self.view.frame)
        imview.image = UIImage(named: "embed2")
    view.addSubview(imview)
       
      
       
       
        view.addSubview(blurview)


But i dont get any blur effect. i am able to see the image at the back through the square, but the rest of the screen is just black in colour. how do i achieve the blur effect . i even tried masking the visualeffectview.mask property , that works only if i present the view controller, if i embed the view controller in a navigationcontroller , i am able to see only the image. why is that?

Hi,

I am facing the same problem. Did you figure out how to do it?

My first instinct would be because you are implementing UIView.draw(rect:). You basically never want to implement that method unless you are using Core Graphics to draw, which you are not here. The work you are doing would be better done in UIView.layoutSubviews().


But assuming you are doing this on iOS 10.x (or lower) this is expected because you are masking the blur's superview. Masking requires an offscreen render pass, and before iOS 11 that breaks blurs.


Now you say this works if you do a modal presentation, but not if you add the view to a navigation controller – how are you doing the latter? I wouldn't expect a significant difference in these two cases unless there is something that I don't understand about your configuration.

Did anyone figure out the proper solution to only rounding the top corners of a UIVisualEffectView in iOS 10? None of these solutions are working for me.

The only solution that can work is to use UIView.maskView on the UIVisualEffectView. But without knowing what you tried its hard to understand where you might have gone wrong.