Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Masked UIVisualEffectView does not work on iOS 10

I have an app I created that uses UIBlurEffectView that worked perfect on iOS 9 and under, but when I upgraded my device (a few of them, not just 1 device) the blur disappeared and instead of the blur there is a half-transperant view for some reason.

Does anything changed in this class? Anyone knows why?

My code (The view is a shape from SVG file that I'm getting using PocketSVG API):

 let blur: UIBlurEffect = UIBlurEffect(style: .Light)
let ev: UIVisualEffectView = UIVisualEffectView(effect: blur)
ev.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(ev)

ev.rightAnchor.constraintEqualToAnchor(self.rightAnchor).active = true
ev.bottomAnchor.constraintEqualToAnchor(self.bottomAnchor).active = true
ev.leftAnchor.constraintEqualToAnchor(self.leftAnchor).active = true
ev.heightAnchor.constraintEqualToAnchor(self.heightAnchor, multiplier: 1.5).active = true

let myPath: CGPathRef = PocketSVG.pathFromSVGFileNamed("CategoriesBar").takeUnretainedValue()

var transform: CGAffineTransform = CGAffineTransformMakeScale(self.frame.size.width / 754.0, self.frame.size.height / 220.0)

let transformedPath: CGPathRef = CGPathCreateMutableCopyByTransformingPath(myPath, &transform)!

let myShapeLayer = CAShapeLayer()
myShapeLayer.path = transformedPath
self.layer.mask = myShapeLayer

Leo Natan's answer code:

What you've suggested doesn't work, here is the code

 override func layoutSubviews() {

        let blur: UIBlurEffect = UIBlurEffect(style: .Light)
        let ev: UIVisualEffectView = UIVisualEffectView(effect: blur)
        ev.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(ev)

        ev.rightAnchor.constraintEqualToAnchor(self.rightAnchor).active = true
        ev.bottomAnchor.constraintEqualToAnchor(self.bottomAnchor).active = true
        ev.leftAnchor.constraintEqualToAnchor(self.leftAnchor).active = true
        ev.heightAnchor.constraintEqualToAnchor(self.heightAnchor, multiplier: 1.5).active = true

        let myPath: CGPathRef = PocketSVG.pathFromSVGFileNamed("CategoriesBar").takeUnretainedValue()

        var transform: CGAffineTransform = CGAffineTransformMakeScale(self.frame.size.width / 754.0, self.frame.size.height / 220.0)

        let transformedPath: CGPathRef = CGPathCreateMutableCopyByTransformingPath(myPath, &transform)!

        let myShapeLayer = CAShapeLayer()
        myShapeLayer.path = transformedPath
        self.layer.mask = myShapeLayer

        let myMaskedView = UIView(frame: ev.frame)
        myMaskedView.layer.mask = myShapeLayer
        ev.maskView = myMaskedView
}

Konrad Siemczyk answer code

override func layoutSubviews() {

        let blur: UIBlurEffect = UIBlurEffect(style: .Light)
        let ev: UIVisualEffectView = UIVisualEffectView(effect: blur)
        ev.frame = self.bounds
        ev.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(ev)

        ev.rightAnchor.constraintEqualToAnchor(self.rightAnchor).active = true
        ev.bottomAnchor.constraintEqualToAnchor(self.bottomAnchor).active = true
        ev.leftAnchor.constraintEqualToAnchor(self.leftAnchor).active = true
        ev.heightAnchor.constraintEqualToAnchor(self.heightAnchor, multiplier: 1.5).active = true

        let myPath: CGPathRef = PocketSVG.pathFromSVGFileNamed("CategoriesBar").takeUnretainedValue()
        var transform: CGAffineTransform = CGAffineTransformMakeScale(self.frame.size.width / 754.0, self.frame.size.height / 220.0)

        let transformedPath: CGPathRef = CGPathCreateMutableCopyByTransformingPath(myPath, &transform)!

        let myShapeLayer = CAShapeLayer()
        myShapeLayer.path = transformedPath
        //self.layer.mask = myShapeLayer
        myShapeLayer.fillRule = kCAFillRuleEvenOdd

        let myMaskedView = UIView(frame: self.frame)
        myMaskedView.backgroundColor = UIColor.blackColor()
        myMaskedView.layer.mask = myShapeLayer
        ev.maskView = myMaskedView
    }
like image 483
FS.O6 Avatar asked Sep 16 '16 07:09

FS.O6


3 Answers

Thanks to @emrahgunduz answer I managed to update the above code to Swift 5.2.

import UIKit
import PlaygroundSupport

let generalFrame = CGRect(x: 0, y: 0, width: 500, height: 500)
let containerView = UIView(frame: generalFrame)
containerView.backgroundColor = UIColor.black;

PlaygroundPage.current.liveView = containerView

let parentView = UIView(frame: generalFrame)
containerView.addSubview(parentView)

let url = URL(string: "https://static.pexels.com/photos/168066/pexels-photo-168066-large.jpeg")
let data = try Data(contentsOf: url!);

let imageView = UIImageView(frame:parentView.bounds)
imageView.image = UIImage(data: data)
imageView.contentMode = .scaleAspectFill
var  roundedRect = CGRect (
    x: 0.0,
    y: 0.0,
    width: parentView.bounds.size.width * 0.5,
    height: parentView.bounds.size.width * 0.5
);
roundedRect.origin.x = parentView.frame.size.width / 2 - roundedRect.size.width / 2;
roundedRect.origin.y = parentView.frame.size.height / 2 - roundedRect.size.height / 2;
let cornerRadius = roundedRect.size.height / 2.0;
let path = UIBezierPath(rect:parentView.bounds)
let croppedPath = UIBezierPath(roundedRect: roundedRect, cornerRadius: cornerRadius)
path.append(croppedPath)
path.usesEvenOddFillRule = true

let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath;
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
let blurView = UIBlurEffect(style: .light)
let effectView = UIVisualEffectView(effect: blurView)

effectView.frame = generalFrame
effectView.layer.mask = maskLayer

parentView.addSubview(imageView)
parentView.addSubview(effectView)

enter image description here

like image 102
Anita Avatar answered Sep 28 '22 19:09

Anita



Hey, before implementing this one...

TLDR: Please check this solution first:

https://stackoverflow.com/a/67939549/2829540

... even though these examples work on older versions of iOS, looks like newer ones require a layer instead of a view, this answer might not work as expected. You might need to implement this solution for older versions and the linked one for newer ones.


For ObjectiveC users out there.

Here is a working example for iOS 10. I also attached the resulting view at the end. I am adding the white border on top later. The cropped circle masking is in the code, if you like it use it as is.

// "self" in here is an UIView that contains some images inside.
{
  UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
  UIVisualEffectView *blurredEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];

  CGRect frame = self.frame;
  frame.origin = CGPointMake (0, 0);

  blurredEffectView.frame = frame;
  [self addSubview:blurredEffectView];

  UIView *maskView = [[UIView alloc] initWithFrame:frame];
  maskView.backgroundColor = [UIColor blackColor];

  __weak UIView *weak = self;
  maskView.layer.mask = ({ // This mask draws a rectangle and crops a circle inside it.
    __strong UIView *strong = weak;

    CGRect roundedRect = CGRectMake (
      0,
      0,
      strong.frame.size.width * 0.8f,
      strong.frame.size.width * 0.8f
    );
    roundedRect.origin.x = strong.frame.size.width / 2 - roundedRect.size.width / 2;
    roundedRect.origin.y = strong.frame.size.height / 2 - roundedRect.size.height / 2;

    CGFloat cornerRadius = roundedRect.size.height / 2.0f;

    UIBezierPath *path        = [UIBezierPath bezierPathWithRect:self.bounds];
    UIBezierPath *croppedPath = [UIBezierPath bezierPathWithRoundedRect:roundedRect cornerRadius:cornerRadius];
    [path appendPath:croppedPath];
    [path setUsesEvenOddFillRule:YES];

    CAShapeLayer *mask = [CAShapeLayer layer];
    mask.path     = path.CGPath;
    mask.fillRule = kCAFillRuleEvenOdd;
    mask;
  });

  blurredEffectView.maskView = maskView;
}

So, this is same code as Swift 3 for testing in playground.

This is using a try while downloading the url, so it is synchronous

import UIKit
import PlaygroundSupport

let generalFrame = CGRect(x: 0, y: 0, width: 500, height: 500)

let containerView = UIView(frame: generalFrame)
containerView.backgroundColor = UIColor.black;
PlaygroundPage.current.liveView = containerView

let parentView = UIView(frame: generalFrame)
containerView.addSubview(parentView)

let url = URL(string: "https://static.pexels.com/photos/168066/pexels-photo-168066-large.jpeg")
let data = try Data(contentsOf: url!);

let imageView = UIImageView(frame:parentView.bounds)
imageView.image = UIImage(data: data)
imageView.contentMode = .scaleAspectFill

let maskView = UIView(frame:parentView.bounds)
maskView.backgroundColor = UIColor.black
maskView.layer.mask = {() -> CALayer in
    var  roundedRect = CGRect (
        x: 0.0,
        y: 0.0,
        width: parentView.bounds.size.width * 0.5,
        height: parentView.bounds.size.width * 0.5
    );
    roundedRect.origin.x = parentView.frame.size.width / 2 - roundedRect.size.width / 2;
    roundedRect.origin.y = parentView.frame.size.height / 2 - roundedRect.size.height / 2;
    
    let cornerRadius = roundedRect.size.height / 2.0;
    
    let path = UIBezierPath(rect:parentView.bounds)
    let croppedPath = UIBezierPath(roundedRect: roundedRect, cornerRadius: cornerRadius)
    path.append(croppedPath)
    path.usesEvenOddFillRule = true
    
    let maskLayer = CAShapeLayer()
    maskLayer.path = path.cgPath;
    maskLayer.fillRule = kCAFillRuleEvenOdd
    return maskLayer
}()

let blurView = UIBlurEffect(style: .light)
let effectView = UIVisualEffectView(effect: blurView)
effectView.frame = generalFrame

effectView.mask = maskView
parentView.addSubview(imageView)
parentView.addSubview(effectView)

And working example in a view controller:

This one downloads an image first then appends the blur effect.

import UIKit

class ViewController: UIViewController {

  func addTheBlurView(data :Data) {

    let generalFrame = self.view.bounds;
    let parentView = UIView(frame: generalFrame)
    self.view.addSubview(parentView)

    let imageView = UIImageView(frame: parentView.bounds)
    imageView.image = UIImage(data: data)
    imageView.contentMode = .scaleAspectFill

    let maskView = UIView(frame: parentView.bounds)
    maskView.backgroundColor = UIColor.black
    maskView.layer.mask = {
      () -> CALayer in
      var roundedRect = CGRect(
          x: 0.0,
          y: 0.0,
          width: parentView.bounds.size.width * 0.5,
          height: parentView.bounds.size.width * 0.5
          );
      roundedRect.origin.x = parentView.frame.size.width / 2 - roundedRect.size.width / 2;
      roundedRect.origin.y = parentView.frame.size.height / 2 - roundedRect.size.height / 2;

      let cornerRadius = roundedRect.size.height / 2.0;

      let path = UIBezierPath(rect: parentView.bounds)
      let croppedPath = UIBezierPath(roundedRect: roundedRect, cornerRadius: cornerRadius)
      path.append(croppedPath)
      path.usesEvenOddFillRule = true

      let maskLayer = CAShapeLayer()
      maskLayer.path = path.cgPath;
      maskLayer.fillRule = kCAFillRuleEvenOdd
      return maskLayer
    }()

    let blurView = UIBlurEffect(style: .light)
    let effectView = UIVisualEffectView(effect: blurView)
    effectView.frame = generalFrame

    effectView.mask = maskView
    parentView.addSubview(imageView)
    parentView.addSubview(effectView)

  }

  override func viewDidLoad() {
    debugPrint("Running...")

    super.viewDidLayoutSubviews();

    // Lets load an image first, so blur looks cool
    let url = URL(string: "https://static.pexels.com/photos/168066/pexels-photo-168066-large.jpeg")

    URLSession.shared.dataTask(with: url!) {
      (data, response, error) in

      if error != nil {
        print(error)
        return
      }

      DispatchQueue.main.async(execute: {
        self.addTheBlurView(data: data!)
      })

    }.resume()

  }
}

OBJECTIVEC VERSION

Works on simulator and the device

PLAYGROUND VERSION

Works in playground

VIEWCONTROLLER VERSION

Works on simulator and device

like image 32
emrahgunduz Avatar answered Sep 28 '22 18:09

emrahgunduz


According to a discussion with Apple engineer, this is a limitation to how the UIVisualEffectView works. It used to work before, but UIVisualEffectView was less accurate.

The suggested approach in the discussion is to use maskView instead of masking the layer directly. So try creating a view, mask that view's layer, and set that as the mask view.

let myMaskedView = UIView(frame: ev.frame)
myMaskedView.layer.mask = myShapeLayer
ev.maskView = myMaskedView
like image 20
Léo Natan Avatar answered Sep 28 '22 18:09

Léo Natan