Search code examples
swiftcalayermaskcashapelayer

Swift how to mask shape layer to blur layer


I was making a progress circle, and I want its track path to have a blur effect, is there any way to achieve that?

This is what the original track looks like(the track path is transparent, I want it to be blurred)

enter image description here

And this is my attempt
And this is my attempt

let outPutViewFrame = CGRect(x: 0, y: 0, width: 500, height: 500)
let circleRadius: CGFloat = 60
let circleViewCenter = CGPoint(x: outPutViewFrame.width / 2 , y: outPutViewFrame.height / 2)
let circleView = UIView()
let progressWidth: CGFloat = 8

circleView.frame.size = CGSize(width: (circleRadius + progressWidth) * 2, height: (circleRadius + progressWidth) * 2)
circleView.center = circleViewCenter


let circleTrackPath = UIBezierPath(arcCenter: CGPoint(x: circleView.frame.width / 2, y: circleView.frame.height / 2), radius: circleRadius, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
let blur = UIBlurEffect(style: .light)
let blurEffect = UIVisualEffectView(effect: blur)
blurEffect.frame = circleView.bounds
blurEffect.mask(withPath: circleTrackPath, inverse: false)

extension UIView {

    func mask(withPath path: UIBezierPath, inverse: Bool = false) {
    
        let maskLayer = CAShapeLayer()

        if inverse {
            path.append(UIBezierPath(rect: self.bounds))
            maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
        }
        
        maskLayer.path = path.cgPath
        maskLayer.lineWidth = 5
        maskLayer.lineCap = CAShapeLayerLineCap.round
       

        self.layer.mask = maskLayer
    }
}

Solution

    • Set maskLayer.fillRule to evenOdd, even when not inversed.

        if inverse {
            path.append(UIBezierPath(rect: self.bounds))
        }
        maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
      
    • create the circleTrackPath by using a big circle and a smaller circle.

        let circleCenter = CGPoint(x: circleView.frame.width / 2, y: circleView.frame.height / 2)
        let circleTrackPath = UIBezierPath(ovalIn: 
            CGRect(origin: circleCenter, size: .zero)
                .insetBy(dx: circleRadius, dy: circleRadius))
        // smaller circle
        circleTrackPath.append(CGRect(origin: circleCenter, size: .zero)
                .insetBy(dx: circleRadius * 0.8, dy: circleRadius * 0.8))
      
    • Set circleTrackPath.usesEvenOddFillRule to true:

        circleTrackPath.usesEvenOddFillRule = true
      

    Now you have a blurred full circle. The non-blurred arc part can be implemented as another sublayer.

    Here is a MCVE that you can paste into a playground:

    let container = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
    
    // change this to a view of your choice
    let image = UIImageView(image: UIImage(named: "my_image"))
    let blur = UIVisualEffectView(effect: UIBlurEffect(style: .light))
    container.addSubview(image)
    blur.frame = image.frame
    container.addSubview(blur)
    let outer = image.bounds.insetBy(dx: 30, dy: 30)
    let path = UIBezierPath(ovalIn: outer)
    path.usesEvenOddFillRule = true
    path.append(UIBezierPath(ovalIn: outer.insetBy(dx: 10, dy: 10)))
    
    let maskLayer = CAShapeLayer()
    maskLayer.path = path.cgPath
    maskLayer.fillRule = .evenOdd
    blur.layer.mask = maskLayer
    container // <--- playground quick look this
    

    Using my profile pic as the background, this produces:

    enter image description here