Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw hole on UIBlurEffect

Xcode 8.0 - Swift 2.3
I have an internal extension to create blur layer that works great:

internal extension UIView {
    
    /**
     Add and display on current view a blur effect.
     */
    internal func addBlurEffect(style style: UIBlurEffectStyle = .ExtraLight, atPosition position: Int = -1) -> UIView {
        // Blur Effect
        let blurEffectView = self.createBlurEffect(style: style)
        if position >= 0 {
            self.insertSubview(blurEffectView, atIndex: position)
        } else {
            self.addSubview(blurEffectView)
        }
        return blurEffectView
    }
 
    internal func createBlurEffect(style style: UIBlurEffectStyle = .ExtraLight) -> UIView {
        let blurEffect = UIBlurEffect(style: style)
        let blurEffectView = UIVisualEffectView(effect: blurEffect)
        blurEffectView.frame = self.bounds
        blurEffectView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
        return blurEffectView
    }
    
}

Question is: how can I add a shaped hole in blur overlay? I have made many attempts:

let p = UIBezierPath(roundedRect: CGRectMake(0.0, 0.0, self.viewBlur!.frame.width, self.viewBlur!.frame.height), cornerRadius: 0.0)
p.usesEvenOddFillRule = true
let f = CAShapeLayer()
f.fillColor = UIColor.redColor().CGColor
f.opacity = 0.5
f.fillRule = kCAFillRuleEvenOdd
p.appendPath(self.holePath)
f.path = p.CGPath
self.viewBlur!.layer.addSublayer(f)

but result is:

enter image description here

I can't understand why hole is ok on UIVisualEffectView but not in _UIVisualEffectBackdropView

UPDATE

I've tryied @Arun solution (with UIBlurEffectStyle.Dark), but result is not the same:

enter image description here

UPDATE 2

With @Dim_ov's solution I have: enter image description here

In order to make this work I need to hide _UIVisualEffectBackdropViewin this way:

    for v in effect.subviews {
        if let filterView = NSClassFromString("_UIVisualEffectBackdropView") {
            if v.isKindOfClass(filterView) {
                v.hidden = true
            }
        }
    }
like image 598
Luca Davanzo Avatar asked Oct 26 '16 08:10

Luca Davanzo


2 Answers

In iOS 10 you have to use mask property of UIVisualEffectView instead of CALayer's mask.

I saw this covered in the release notes for some early betas of iOS 10 or Xcode 8, but I can not find those notes now :). I'll update my answer with a proper link as soon as I find it.

So here is the code that works in iOS 10/Xcode 8:

class ViewController: UIViewController {
    @IBOutlet var blurView: UIVisualEffectView!

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        updateBlurViewHole()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        updateBlurViewHole()
    }

    func updateBlurViewHole() {
        let maskView = UIView(frame: blurView.bounds)
        maskView.clipsToBounds = true;
        maskView.backgroundColor = UIColor.clear

        let outerbezierPath = UIBezierPath.init(roundedRect: blurView.bounds, cornerRadius: 0)
        let rect = CGRect(x: 150, y: 150, width: 100, height: 100)
        let innerCirclepath = UIBezierPath.init(roundedRect:rect, cornerRadius:rect.height * 0.5)
        outerbezierPath.append(innerCirclepath)
        outerbezierPath.usesEvenOddFillRule = true

        let fillLayer = CAShapeLayer()
        fillLayer.fillRule = kCAFillRuleEvenOdd
        fillLayer.fillColor = UIColor.green.cgColor // any opaque color would work
        fillLayer.path = outerbezierPath.cgPath
        maskView.layer.addSublayer(fillLayer)

        blurView.mask = maskView;
    }
}

Swift 2.3 version:

class ViewController: UIViewController {
    @IBOutlet var blurView: UIVisualEffectView!

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)

        updateBlurViewHole()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        updateBlurViewHole()
    }

    func updateBlurViewHole() {
        let maskView = UIView(frame: blurView.bounds)
        maskView.clipsToBounds = true;
        maskView.backgroundColor = UIColor.clearColor()

        let outerbezierPath = UIBezierPath.init(roundedRect: blurView.bounds, cornerRadius: 0)
        let rect = CGRect(x: 150, y: 150, width: 100, height: 100)
        let innerCirclepath = UIBezierPath.init(roundedRect:rect, cornerRadius:rect.height * 0.5)
        outerbezierPath.appendPath(innerCirclepath)
        outerbezierPath.usesEvenOddFillRule = true

        let fillLayer = CAShapeLayer()
        fillLayer.fillRule = kCAFillRuleEvenOdd
        fillLayer.fillColor = UIColor.greenColor().CGColor
        fillLayer.path = outerbezierPath.CGPath
        maskView.layer.addSublayer(fillLayer)

        blurView.maskView = maskView
    }
}

UPDATE

Well, it was Apple Developer forums discussion and not iOS release notes. But there are answers from Apple's representatives so I think, this information may be considered "official".

A link to the discussion: https://forums.developer.apple.com/thread/50854#157782

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.

enter image description here

like image 199
Dim_ov Avatar answered Oct 10 '22 01:10

Dim_ov


Please check my code here

internal extension UIView {

/**
 Add and display on current view a blur effect.
 */
internal func addBlurEffect(style style: UIBlurEffectStyle = .ExtraLight, atPosition position: Int = -1) -> UIView {
    // Blur Effect
    let blurEffectView = self.createBlurEffect(style: style)
    if position >= 0 {
        self.insertSubview(blurEffectView, atIndex: position)
    } else {
        self.addSubview(blurEffectView)
    }
    return blurEffectView
}

internal func createBlurEffect(style style: UIBlurEffectStyle = .ExtraLight) -> UIView {
    let blurEffect = UIBlurEffect(style: style)
    let blurEffectView = UIVisualEffectView(effect: blurEffect)
    blurEffectView.frame = self.bounds
    blurEffectView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
    return blurEffectView
}

}

class ViewController: UIViewController {

@IBOutlet weak var blurView: UIImageView!

var blurEffectView: UIView!

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    blurEffectView = blurView.addBlurEffect(style: UIBlurEffectStyle.Light, atPosition: 0)


}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    let outerbezierPath = UIBezierPath.init(roundedRect: self.blurEffectView.frame, cornerRadius: 0)
    let rect = CGRectMake(150, 150, 100, 100)
    let innerCirclepath = UIBezierPath.init(roundedRect:rect, cornerRadius:rect.height * 0.5)
    outerbezierPath.appendPath(innerCirclepath)
    outerbezierPath.usesEvenOddFillRule = false
    let fillLayer = CAShapeLayer()
    fillLayer.fillRule = kCAFillRuleEvenOdd
    fillLayer.fillColor = UIColor.blackColor().CGColor
    fillLayer.path = outerbezierPath.CGPath
    self.blurEffectView.layer.mask = fillLayer
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

} With UIBlurEffectStyle.Light enter image description here

With UIBlurEffectStyle.Dark enter image description here

With UIBlurEffectStyle.ExtraLight enter image description here

like image 34
Arun Ammannaya Avatar answered Oct 10 '22 01:10

Arun Ammannaya