jadengeller jadengeller - 5 months ago 28
Objective-C Question

UIBezierPath Subtract Path

By using

[UIBezierPath bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:]
, I am able to create a rounded view, such as this:

rounded view

How could I subtract another path from this one (or some other way), to create a path like this:

subtracted view

Is there any way I can do something like this?
Pseudocode:

UIBezierPath *bigMaskPath = [UIBezierPath bezierPathWithRoundedRect:bigView.bounds
byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight)
cornerRadii:CGSizeMake(18, 18)];
UIBezierPath *smallMaskPath = [UIBezierPath bezierPathWithRoundedRect:smalLView.bounds
byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight)
cornerRadii:CGSizeMake(18, 18)];

UIBezierPath *finalPath = [UIBezierPath pathBySubtractingPath:smallMaskPath fromPath:bigMaskPath];

Answer

If you want to stroke the subtracted path, you are on your own. Apple doesn't provide an API that returns (or just strokes) the subtraction of one path from another.

If you just want to fill the subtracted path (as in your example image), you can do it using the clipping path. You have to use a trick, though. When you add a path to the clipping path, the new clipping path is the intersection of the old clipping path and the added path. So if you just add smallMaskPath to the clipping path, you will end up filling only the region inside smallMaskPath, which is the opposite of what you want.

What you need to do is intersect the existing clipping path with the inverse of smallMaskPath. Fortunately, you can do that pretty easily using the even-odd winding rule. You can read about the even-odd rule in the Quartz 2D Programming Guide.

The basic idea is that we create a compound path with two subpaths: your smallMaskPath and a huge rectangle that completely encloses your smallMaskPath and every other pixel you might want to fill. Because of the even-odd rule, every pixel inside of smallMaskPath will be treated as outside of the compound path, and every pixel outside of smallMaskPath will be treated as inside of the compound path.

So let's create this compound path. We'll start with the huge rectangle. And there is no rectangle more huge than the infinite rectangle:

UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];

Now we make it into a compound path by adding smallMaskPath to it:

[clipPath appendPath:smallMaskPath];

Next we set the path to use the even-odd rule:

clipPath.usesEvenOddFillRule = YES;

Before we clip to this path, we should save the graphics state so that we can undo the change to the clipping path when we're done:

CGContextSaveGState(UIGraphicsGetCurrentContext()); {

Now we can modify the clipping path:

    [clipPath addClip];

and we can fill bigMaskPath:

    [[UIColor orangeColor] setFill];
    [bigMaskPath fill];

Finally we restore the graphics state, undoing the change to the clipping path:

} CGContextRestoreGState(UIGraphicsGetCurrentContext());

Here's the code all together in case you want to copy/paste it:

UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:CGRectInfinite];
[clipPath appendPath:smallMaskPath];
clipPath.usesEvenOddFillRule = YES;

CGContextSaveGState(UIGraphicsGetCurrentContext()); {
    [clipPath addClip];
    [[UIColor orangeColor] setFill];
    [bigMaskPath fill];
} CGContextRestoreGState(UIGraphicsGetCurrentContext());