0

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.

Chris
  • 4,009
  • 3
  • 21
  • 52
  • Here is a video showing current state of the effect and demonstrating the problem with the second circle on the bottom icon: https://www.dropbox.com/s/lvjhf6okgr6n3vn/IMG_3903.TRIM.MOV?raw=1 – Chris Mar 14 '19 at 06:18
  • Couldn't you draw two arcs using addArcWithCenter method and combine them to create a single UIBezierpath? – Tibin Thomas Mar 15 '19 at 08:59
  • @TibinThomas Thanks for this. I had just come across the arc Bézier path method a few minutes ago, and the intersection points of the circles are easy to find. How would I use this method though? Would I need to know the angle of arc of both circles outside the intersection point? – Chris Mar 15 '19 at 09:01
  • yes you would need the exact radius of two circles and also the exact start and end angle for both the circle such that the two comined arcs form a single shape you desire.If the circles are of constant sizes it might be easy to calculate. – Tibin Thomas Mar 15 '19 at 09:05
  • @TibinThomas Yes the circles are constant. Larger one is 25pt radius and smaller one is 10pt radius and aligned to the bottom-right of a square bounding the larger circle. Would you be able to submit a demonstration of linking two arcs, as I think this could be what I’m looking for. – Chris Mar 15 '19 at 09:07
  • also you could try drawing the bigger one first with addArc and the smaller one connecting the two end points of arc using - addQuadCurveToPoint:controlPoint: method. – Tibin Thomas Mar 15 '19 at 09:09

3 Answers3

1

try the following code in your function

 let newRect: CGRect
 if let rect2 = rect2{
    let raw = rect1.union(rect2)
     let size = max(raw.width, raw.height)
     newRect = CGRect(x: raw.minX, y: raw.minX, width: size, height: size)
    }else{
      newRect = rect1
     }

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

Full function

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 newRect: CGRect
        if let rect2 = rect2{
            let raw = rect1.union(rect2)
            let size = max(raw.width, raw.height) // getting the larger value in order to draw a proper circle
            newRect = CGRect(x: raw.minX, y: raw.minX, width: size, height: size)
        }else{
            newRect = rect1
        }

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



        // 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
    }

I used the built-in function union to create a raw CGRect and then I get the max value to draw a proper circle.

Sahil Manchanda
  • 9,812
  • 4
  • 39
  • 89
  • Thanks for this - I’ll try it out later – Chris Mar 14 '19 at 06:16
  • This will draw a single circle around both the circular icons (i.e. a circle within the square that bounds both of them). At present, my smaller optional icon is always contained within the 'square' of the larger one anyway, aligned to the bottom-right. – Chris Mar 15 '19 at 20:51
1

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.

Tibin Thomas
  • 1,365
  • 2
  • 16
  • 25
  • Thanks very much. I’ll test it later. Do the points have to be in the context of my imageView’s immediate superview? – Chris Mar 15 '19 at 10:43
  • Also, I think I can convert this to Swift quite easily, but are you aware of any differences in Swift with these methods? – Chris Mar 15 '19 at 10:44
  • 1
    You would be able to convert to swift easily as there are no differences.But I am not sure about the context of points you should use.My assumption is you should use the context of your effectview's frame. – Tibin Thomas Mar 15 '19 at 10:53
  • No problem. Given that I already draw a Bézier curve the same context should work. Thanks – Chris Mar 15 '19 at 10:54
  • 1
    Please refer @chris answer for the perfect solution. – Tibin Thomas Mar 16 '19 at 02:35
1

Thanks to the accepted answer (from user Tibin Thomas) I was able to adapt the use of arcs with UIBezierPath to obtain exactly what I needed. I have accepted his answer but posted my final code here for future reference.

Of note, before calling this method, I have to convert the CGRect coordinates from the UIImageViews superview to the coordinate space of my UIEffectView. I also apply an inset of -1 to obtain the 1pt border. Thus the radii used are 1 greater than the radii of my UIImageViews.

Masked icons

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)

    // Center of rect1
    let x1 = rect1.midX
    let y1 = rect1.midY
    let center1 = CGPoint(x: x1, y: y1)

    // New path
    let newPath: UIBezierPath

    if let rect2 = rect2 {
        // Need to get two arcs - main icon and padlock icon
        // Center of rect2
        let x2 = rect2.midX
        let y2 = rect2.midY
        let center2 = CGPoint(x: x2, y: y2)
        // These values are hard-coded for 25pt radius main icon and bottom-right-aligned 10pt radius padlock icon with a 1pt additional border
        newPath = UIBezierPath(arcCenter: center1, radius: 26, startAngle: 1.2, endAngle: 0.3, clockwise: true)
        newPath.addArc(withCenter: center2, radius: 11, startAngle: 5.8, endAngle: 2.2, clockwise: true)
    } else {
        // Only single circle is needed
        newPath = UIBezierPath(ovalIn: rect1)
    }

    newPath.close()

    newClipPath.append(newPath)

    // 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
}
Chris
  • 4,009
  • 3
  • 21
  • 52