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

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?

Same issue here. I get what you guys are saying, but for me its a transition that uses a mask and I can't seem to get it to work - where I know it was said it didn't really work in iOS 9 - but the hundreds of times I used it, it seemed to work.


class FSShapeMaskView : UIView {
  override class var layerClass: Swift.AnyClass {
  get {
       return CAShapeLayer.self
  }
  }
  override open var layer: CALayer {
       return shapeLayer
  }
  var shapeLayer : CAShapeLayer!
  init(shape: CAShapeLayer) {
       shapeLayer = shape
       super.init(frame: CGRect.zero)
  }


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


then ( the forums don't let me post my code for some reason…)


  let maskLayer = CAShapeLayer()
  maskLayer.path = circleMaskPathFinal.cgPath
  let maskView = FSShapeMaskView(shape: maskLayer)
  toViewController.view.mask = maskView

  let maskLayerAnimation = CABasicAnimation(keyPath: "path")
  maskLayerAnimation.fromValue = circleMaskPathInitial.cgPath
  maskLayerAnimation.toValue = circleMaskPathFinal.cgPath
  maskLayerAnimation.duration = self.transitionDuration(using: transitionContext)
  maskLayerAnimation.delegate = self
  maskLayer.add(maskLayerAnimation, forKey: "path")
  toViewController.view.frame = fromViewController.view.frame


After 5 days of Swift3 conversion, perhaps my brain is mush - but do you have any suggestions to move me forward?

I should add it looks like this:


UINavigationController

|

|--- UIView

|

|

UIViewController

|

|---- UIView - this is who is getting the mask

|

|--- UIVisualEffectsView

Transitions are tough in this realm, as maskView is the only way to ensure that the effect works, but maskView also isn't animatable.


What you might try instead – if possible – is to invert the masking. Instead of masking out the visual effect, copy in the content you want to cover the effect. That is, you do something like this:


ImageView

VisualEffectView

ImageView <with inverted mask>


Also do not override UIView.layer. I cannot overemphasize how often you will encounter strange bugs when you do so. Most of UIView accesses its ivar directly, meaning that your override will not apply when interacting with UIView, but also many other parts of UIKit will also interact directly with the UIView layer ivar directly for performance reasons.


It is never a good idea to override UIView.layer. It is also unnecessary to do so – when you override layerClass, we construct the layer with your given class, so UIView's own layer is of the layer class that you want. If you want to get back to the CoreAnimation behavior of all actions animate, then override UIView.action(for:forKey:) and return nil.

@blackjack 's code worked for me. I was on the right path, what i was missing was:


maskView.backgroundColor = [UIColor blackColor];


Code also works if you override didMoveToSuperview method, too.

Can you elaborate on this a bit? I haven't seen anything in the documentation on Core Animation that mentions that nested masking is problematic. Why would it be? (Just curious.) If there's a layer tree with multiple masks at various levels, wouldn't it need to just perform offscreen renders of each subtree that has a mask then composite the final result? Not saying it's efficient, but I don't understand why this would be particularly problematic.