11

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
    }
FS.O6
  • 1,394
  • 2
  • 20
  • 42

5 Answers5

6

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

emrahgunduz
  • 1,404
  • 1
  • 13
  • 26
  • 1
    Doesn't work for me... Can you please check it according to my code and tell me what's wrong? – FS.O6 Sep 21 '16 at 12:07
  • this code is objc, i think you are working with swift? if you are copy pasting this, it won't work for you. where is your code by the way? – emrahgunduz Sep 21 '16 at 12:31
  • I've rewritten your code in Swift, I didn't copy-pasted it :). And sorry, I forgot to add my code on the question ;), refresh this page in 10 seconds. Thanks :) – FS.O6 Sep 21 '16 at 12:33
  • first you are not setting a frame for your UIVisualEffectView, set it to self.bounds and see if this solves the problem... second, try giving your myMaskedView a background color of black... – emrahgunduz Sep 21 '16 at 12:53
  • Still doesn't work.. Note: this code (the first one, not the answer code) works on iOS 9 and under. Note 2: the code is written in `layoutSubviews()`, maybe I should write it somewhere else? Thanks! – FS.O6 Sep 21 '16 at 12:59
  • layoutSubviews should be fine. you can try it in didMoveToSuperview maybe... In ios9 and before, UIVisualEffectView mask was not always performing as it should, that is why it is changed. On most devices it was capturing/blurring some wrong view, or it was working but was causing too much performance + render problems. – emrahgunduz Sep 21 '16 at 13:03
  • When I move it to `didMoveToSuperview()` the view doesn't even shown – FS.O6 Sep 21 '16 at 13:05
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/123868/discussion-between-emrahgunduz-and-fs-o6). – emrahgunduz Sep 21 '16 at 13:06
  • you are definitely doing something wrong. create a new ios app in xcode and try the viewcontroller i added here. – emrahgunduz Sep 21 '16 at 19:53
  • Thanks @emrahgunduz! I've used your code to mask a VisualEffectView that I use as a chrome in a presentation controller. It works perfectly until I rotate the device. I tried resetting the blur views mask view, resetting the mask view's mask and just updating the mask view's mask's path. The result was either a completely transparent blur view or a blur view without any changes to the path (looks like before rotation, just transformed). Do you have any idea? – fruitcoder Jan 05 '17 at 12:05
  • @fruitcoder were you able to solve the problem? probably most easy way is creating a new mask view instead of trying to update one. – emrahgunduz Feb 01 '17 at 11:43
  • is it still working? I have tried the playground version using Swift 5.2, but I just got the blue image. The mask is not showing? – Kevin Amiranoff Jun 11 '21 at 13:04
  • @KevinAmiranoff looks like it is broken in current iOS version. i'll try to find and update the solution when i get some free time. thanks for reporting. – emrahgunduz Jun 18 '21 at 09:52
  • 1
    @emrahgunduz Thanks. We found a solution in the end. Check the answer below: https://stackoverflow.com/a/67939549/2829540 . We got the hint from that answer: https://stackoverflow.com/a/39597884/2829540 – Kevin Amiranoff Jun 18 '21 at 09:54
2

If you're looking for a solution with masked view and blur in Swift 3.0, look at code below:

class func addBlurredView(_ forView: UIView, centeredElement: UIView) {
    let blur = UIVisualEffectView(effect: UIBlurEffect(style: .light))
    blur.frame = forView.frame
    blur.isUserInteractionEnabled = false
    forView.addSubview(blur)

    let radius = centeredElement.bounds.width / 2
    let path = UIBezierPath (
        roundedRect: blur.frame,
        cornerRadius: 0)
    let circle = UIBezierPath (
        roundedRect: CGRect(origin: CGPoint(x: centeredElement.frame.origin.x, y: centeredElement.frame.origin.y),
                             size: centeredElement.frame.size), cornerRadius: radius)
    path.append(circle)
    path.usesEvenOddFillRule = true

    let maskLayer = CAShapeLayer()
    maskLayer.path = path.cgPath
    maskLayer.fillRule = kCAFillRuleEvenOdd

    let maskView = UIView(frame: forView.frame)
    maskView.backgroundColor = UIColor.black
    maskView.layer.mask = maskLayer

    blur.mask = maskView
}
Konrad Siemczyk
  • 186
  • 2
  • 9
2

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

Anita
  • 2,741
  • 27
  • 28
1

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
Léo Natan
  • 56,823
  • 9
  • 150
  • 195
  • I didn't really understood you, can you please add a code example based on the code I posted above? Thank you very much! – FS.O6 Sep 16 '16 at 11:06
  • I tested, and can confirm this works with a mask view. – Léo Natan Sep 16 '16 at 11:30
  • That doesn't work for me, I've updated my question with a new code using you answer, can you please check it and tell me if I missed something? Thanks – FS.O6 Sep 16 '16 at 11:46
  • Your problem is likely due to not giving the mask view a correct frame. You are using auto layout, so you may need to either calculate the target size before, or set the mask view in `viewDidLayoutSubviews`, when the layout pass has finished. Note, that if you need to resize the visual effect view (and mask), you need to set the `maskView` property again, after resizing. – Léo Natan Sep 16 '16 at 11:51
  • I didn't understood you, can you please update your answer with a code? – FS.O6 Sep 16 '16 at 11:52
  • I will not spoon-feed you. I have provided all information necessary for you to solve it. I verified the solution on my end, and it works. – Léo Natan Sep 16 '16 at 11:53
  • But I didn't understood what you mean, what is wrong with my code? – FS.O6 Sep 16 '16 at 11:54
  • Can you please? – FS.O6 Sep 17 '16 at 12:52
  • 1
    I confirm that it is working in iOS 8 and iOS 10, but not working for iOS 9 (tested on device and simulator on iOS 9.3.5) – Nicolas Buquet Sep 27 '16 at 09:56
-1

This code is tested and it is work on Ipad Air 2 simulator iOS 10.0

class BlurView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.initialView()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.initialView()
    }

    func initialView() {
        if UIAccessibilityIsReduceTransparencyEnabled() == false {
            self.backgroundColor = UIColor.clearColor()
            let blurEffect = UIBlurEffect(style: .Dark)

            let blurEffectView = UIVisualEffectView(effect: blurEffect)
            blurEffectView.frame = self.bounds
            blurEffectView.translatesAutoresizingMaskIntoConstraints = true
            blurEffectView.autoresizingMask = UIViewAutoresizing.FlexibleWidth.union(.FlexibleHeight)

            self.addSubview(blurEffectView)
        } else {
            self.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5)
        }
    }
}

You can use it on storyboard or create it using code.

lee5783
  • 434
  • 4
  • 12
  • So the only way is to use a 3rd party API instead of the `UIVisualEffectView`? – FS.O6 Sep 16 '16 at 10:26
  • iPad Retina has blur effects disabled, just like iPhone 4 and iPad 2. Run your code on an iPad Air and above simulator, and it will work as expected. iPad Retina will also not receive iOS 10 update, so it is also unrelated to the question here. – Léo Natan Sep 16 '16 at 10:30
  • @LeoNatan I run it on iPhone 6S and it still doesn't work – FS.O6 Sep 16 '16 at 10:37
  • @FS.O6: I update my answer, You can use it on storyboard or by code – lee5783 Sep 19 '16 at 03:52
  • @lee5783 That's just give me a half-black view (`self.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5)`) – FS.O6 Sep 19 '16 at 09:22
  • @lee5783 Yes, for some reason `UIAccessibilityIsReduceTransparencyEnabled()` returns `true`. But if I go to `settings -> general -> accessibility -> increase contrast -> reduce transparency` (on the device) the switch is off. Any idea why? – FS.O6 Sep 20 '16 at 04:08
  • It's off by default, you should see blur effect, so strange – lee5783 Sep 20 '16 at 04:13