CGPathContainsPoint broken in iOS 12? Is a workaround possible?

Viewed 291

I will try to keep this short.

Take the following test code:

CGPath* path =  CGPathCreateMutable();
CGPathMoveToPoint(path, nil, 318.43, 183.47);
CGPathAddCurveToPoint(path, nil, 322.98, 189.32, 327, 194.93, 329.81, 201.62);
CGPathAddLineToPoint(path, nil, 339.36, 182.58);
CGPathCloseSubpath(path);
CGPathMoveToPoint(path, nil, 327.8, 193.04);
CGPathAddCurveToPoint(path, nil, 323.06, 193.04, 323.06, 185.5, 327.8, 185.5);
CGPathAddCurveToPoint(path, nil, 332.54, 185.5, 332.54, 193.04, 327.8, 193.04);
CGPathCloseSubpath(path);
CGRect boundingBox = CGPathGetBoundingBox(path);

CGPoint testPoint = CGPointMake(200,185);

NSLog(@"path contains point=%d | bounding box contains point=%d",CGPathContainsPoint(path, nil,  testPoint, NO), CGRectContainsPoint(boundingBox, testPoint) );

This code returns correctly on iOS 10:

path contains point=0 | bounding box contains point=0

The same code on iOS 12 returns:

path contains point=1 | bounding box contains point=0

As you can see, on iOS 12 CGPathContainsPoint says my testPoint is inside the shape even if the bounding box of the shape says it is not.

Changing the fill rule to NO in CGPathContainsPoint does not change anything.

Is there a solution to this? I tried apple forums before but nobody seems to answer there.

Thank you for any help!

2 Answers

Confirmed this issue on the same simulator with iOS 10.3 and 12.1

I do not get too why this happens, the only clue is if we remove this lines issue disappears

CGPathMoveToPoint(path, nil, 327.8, 193.04);
CGPathAddCurveToPoint(path, nil, 323.06, 193.04, 323.06, 185.5, 327.8, 185.5);
CGPathAddCurveToPoint(path, nil, 332.54, 185.5, 332.54, 193.04, 327.8, 193.04);
CGPathCloseSubpath(path);

It may be caused by fill rule or whatever (for example by paths overlapping), anyway workaround is to create separate paths and union their bounding boxes afterwards as follows (added some drawing code to visualize the things)

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGPoint testPoint = CGPointMake(200,185);
    [[UIColor blueColor] setStroke];
    CGContextStrokeEllipseInRect(context, CGRectMake(testPoint.x - 5, testPoint.y - 5, 10, 10));

    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, nil, 318.43, 183.47);
    CGPathAddCurveToPoint(path, nil, 322.98, 189.32, 327, 194.93, 329.81, 201.62);
    CGPathAddLineToPoint(path, nil, 339.36, 182.58);

    CGRect box1 = CGPathGetBoundingBox(path);

    NSLog(@"path 1 contains point=%d | bounding box contains point=%d", CGPathContainsPoint(path, nil,  testPoint, NO), CGRectContainsPoint(box1, testPoint));

    [[UIColor grayColor] setFill];
    [[UIColor redColor] setStroke];
    CGContextAddPath(context, path);
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathFillStroke);
    CGPathRelease(path);

    path = CGPathCreateMutable();
    CGPathMoveToPoint(path, nil, 327.8, 193.04);
    CGPathAddCurveToPoint(path, nil, 323.06, 193.04, 323.06, 185.5, 327.8, 185.5);
    CGPathAddCurveToPoint(path, nil, 332.54, 185.5, 332.54, 193.04, 327.8, 193.04);

    CGRect box2 = CGPathGetBoundingBox(path);

    NSLog(@"path 2 contains point=%d | bounding box contains point=%d", CGPathContainsPoint(path, nil,  testPoint, NO), CGRectContainsPoint(box2, testPoint));

    [[UIColor yellowColor] setFill];
    [[UIColor greenColor] setStroke];
    CGContextAddPath(context, path);
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathFillStroke);
    CGPathRelease(path);

    CGRect boundingBox = CGRectUnion(box1, box2);
    [[UIColor magentaColor] setStroke];
    CGContextStrokeRect(context, boundingBox);

    NSLog(@"bounding box contains point=%d", CGRectContainsPoint(boundingBox, testPoint));
}

enter image description here

I see the same behavior you describe. Interestingly, if you manually close the path, it seems to work. E.g., this is the same as your path except adding the line with the comment:

CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, 318.43, 183.47);
CGPathAddCurveToPoint(path, nil, 322.98, 189.32, 327, 194.93, 329.81, 201.62);
CGPathAddLineToPoint(path, nil, 339.36, 182.58);
CGPathAddLineToPoint(path, nil, 318.43, 183.47);  // manually add line back to starting point
CGPathCloseSubpath(path);

CGPathMoveToPoint(path, nil, 327.8, 193.04);
CGPathAddCurveToPoint(path, nil, 323.06, 193.04, 323.06, 185.5, 327.8, 185.5);
CGPathAddCurveToPoint(path, nil, 332.54, 185.5, 332.54, 193.04, 327.8, 193.04);
CGPathCloseSubpath(path);

I might suggest trying keeping track of the starting point yourself and adding a line back to it wherever you close the subpath and see if that fixes it.


Even more inexplicably, doing CGPathMoveToPoint twice (lol) also seems to solve it.

CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, 318.43, 183.47);
CGPathMoveToPoint(path, nil, 318.43, 183.47);   // move the the starting point twice?!?
CGPathAddCurveToPoint(path, nil, 322.98, 189.32, 327, 194.93, 329.81, 201.62);
CGPathAddLineToPoint(path, nil, 339.36, 182.58);
CGPathCloseSubpath(path);

CGPathMoveToPoint(path, nil, 327.8, 193.04);
CGPathAddCurveToPoint(path, nil, 323.06, 193.04, 323.06, 185.5, 327.8, 185.5);
CGPathAddCurveToPoint(path, nil, 332.54, 185.5, 332.54, 193.04, 327.8, 193.04);
CGPathCloseSubpath(path);
Related