Search code examples
iosswiftuibezierpathuiblureffect

Masking a UIEffectView with intersecting UIBezierPath


I wanted to apply a UIEffectView with blur over a tableView but have circular UIImageView objects in each cell show through. I used a mask and adapted the solution from this answer to create a method that would iteratively cut out circles above each cell:

func cutCircle(inView view: UIView, rect1: CGRect, rect2: CGRect?) {

    // Create new path and mask
    let newMask = CAShapeLayer()

    // Create path to clip
    let newClipPath = UIBezierPath(rect: view.bounds)

    let path1 = UIBezierPath(ovalIn: rect1)
    newClipPath.append(path1)

    if let rect2 = rect2 {
        let path2 = UIBezierPath(ovalIn: rect2)
        //FIXME: Need a way to get a union of both paths!
        //newClipPath.append(path2)
    }

    // If view already has a mask
    if let originalMask = view.layer.mask, let originalShape = originalMask as? CAShapeLayer, let originalPath = originalShape.path {

        // Create bezierpath from original mask's path
        let originalBezierPath = UIBezierPath(cgPath: originalPath)

        // Append view's bounds to "reset" the mask path before we re-apply the original
        newClipPath.append(UIBezierPath(rect: view.bounds))

        // Combine new and original paths
        newClipPath.append(originalBezierPath)
    }

    // Apply new mask
    newMask.path = newClipPath.cgPath
    newMask.fillRule = .evenOdd
    view.layer.mask = newMask
}

This function is called on the UIEffectView for each visible tableview cell using: for cell in tableView.visibleCells(). It appends a new circle to the mask.

However, some items have a smaller circle icon overlay, like this:

Icon

I added the second CGRect parameter to the method above to conditionally cut out this circle. However, the mask remains intact where the two circles overlap, like this:

Icon with blur effect

I looked at a few answers here, as I needed to find a way to get the union of two UIBezierPath objects. However, this proved very difficult. I don’t think I can use a drawing context as this is a UIEffectView and the mask needs to be cut iteratively.

I tried changing the fill rules (.evenOdd, .nonZero) but this does not have the desired effect.

Are there any tips for combining two overlapping UIBezierPath into a single mask?


The overall aim is to achieve this effect with consecutive tableview cells, but some icons will have the extra circle.

Full effect

Notice how the bottom icon has the extra circle but it is cropped, and my current technique to cut out this extra circle causes the problem noted above, where the overlap is not masked as expected.


Solution

  • You could use addArcWithCenter method to combine two arcs into desired shape.eg:

     #define DEGREES_TO_RADIANS(degrees)((M_PI * degrees)/180)
    
      - (UIBezierPath*)createPath
    {
        UIBezierPath* path = [[UIBezierPath alloc]init];
        [path addArcWithCenter:CGPointMake(25, 25) radius:25 startAngle:DEGREES_TO_RADIANS(30) endAngle:DEGREES_TO_RADIANS(60) clockwise:false];
        [path addArcWithCenter:CGPointMake(40, 40) radius:10 startAngle:DEGREES_TO_RADIANS(330) endAngle:DEGREES_TO_RADIANS(120) clockwise:true];
        [path closePath];
        return path;
    }
    

    I wasn't able to find out the exact points but if you could adjust the constants you could create the perfect shape required.