2

I am trying to use Apples method for detecting if a point is in on a UIBezierPath. However it returns an ‘invalid context’.

As you can see from the NSlog, I am passing a UIBezierPath and A point to check. In my case a touch point.

I don’t understand why. Can someone explain it to me or point me in the correct direction?

NSLOG -----

Path <UIBezierPath: 0x7f57110>
Contains point Path <UIBezierPath: 0x7f57110>
Touch point 425.000000 139.000000
<Error>: CGContextSaveGState: invalid context 0x0
<Error>: CGContextAddPath: invalid context 0x0
<Error>: CGContextPathContainsPoint: invalid context 0x0
<Error>: CGContextRestoreGState: invalid context 0x0
NO

Straight from Apples Documentation on how to determine a point in a path

- (BOOL)containsPoint:(CGPoint)point onPath:(UIBezierPath *)path inFillArea:(BOOL)inFill {

    NSLog(@"contains point Path %@", path);
    NSLog(@"Touch point %f %f", point.x, point.y );

    CGContextRef context = UIGraphicsGetCurrentContext();
    CGPathRef cgPath = path.CGPath;
    BOOL    isHit = NO;
// Determine the drawing mode to use. Default to detecting hits on the stroked portion of the path.
    CGPathDrawingMode mode = kCGPathStroke;

    if (inFill) { // Look for hits in the fill area of the path instead.
        if (path.usesEvenOddFillRule)
            mode = kCGPathEOFill;
        else
            mode = kCGPathFill;
    }
 // Save the graphics state so that the path can be removed later.
    CGContextSaveGState(context);
    CGContextAddPath(context, cgPath);

    // Do the hit detection.
    isHit = CGContextPathContainsPoint(context, point, mode);

    CGContextRestoreGState(context);

    return isHit;
}

Here is my touchesBegan method. I have my paths in an NSMutableArray. I parse the array to check all my paths to see if any has been touched.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

 CGPoint curPoint = [[touches anyObject] locationInView:self];
  for (int i = 0; i < [pathInfo count]; i++){
            NSArray *row = [[NSArray alloc] initWithArray:[pathInfo objectAtIndex:i]];
            UIBezierPath *path = [row objectAtIndex:0];

            NSLog(@"Path %@", path);
if ([self containsPoint:curPoint onPath:path inFillArea:NO]){
               NSLog(@"YES");
            } else {
                NSLog(@"NO");
            }

        }
}
JasonBourne
  • 338
  • 2
  • 13

2 Answers2

4

tl;dr: You should use CGPathContainsPoint( ... ) instead.


What went wrong

Your problem is that you have no context where you are trying to get it

CGContextRef context = UIGraphicsGetCurrentContext(); // <-- This line here...

The method UIGraphicsGetCurrentContext will only return a context if there is a valid current context. The two main examples are

  1. Inside drawRect: (where the context is the view you are drawing into)

  2. Inside you own image context (when you use UIGraphicsBeginImageContext() so that you can use Core Graphics to draw into an image (maybe you pass it to some other part of your code and display it in an image view or save it to disk)).

The solution

I don't know why you were doing all the extra work of contexts, saving and restoring state, etc. I seems that you missed the simple method CGPathContainsPoint().

BOOL isHit = CGPathContainsPoint(
                                 path.CGPath,
                                 NULL,
                                 point,
                                 path.usesEvenOddFillRule
                                 );

Edit

If you want to hit test a stroke path you could use CGPathCreateCopyByStrokingPath() to to first create a new filled path of the path you are stroking (given a certain width etc.). Ole Begemann has a very good explanation on his blog about how to do it (including some sample code).

David Rönnqvist
  • 56,267
  • 18
  • 167
  • 205
  • JasonBourne is using the `kCGPathStroke` mode when he calls `CGContextPathContainsPoint`. The `CGPathContainsPoint` doesn't take a mode parameter, but effectively uses either `kCGPathFill` or `kCGPathEOFill`, so by itself it is not sufficient to replicate the behavior of `CGContextPathContainsPoint`. – rob mayoff Mar 11 '13 at 21:18
  • I missed that first part about the drawing mode – David Rönnqvist Mar 11 '13 at 22:08
  • This shouldn't be that complicated. Is there not a simple way to determine if a Path is in the area of a touched point without me re-writing my entire program to compensate for this one detail? @robmayoff – JasonBourne Mar 12 '13 at 17:36
  • 1
    Which part is complicated? When you say “if a Path is in the area of a touched point”, what do you mean? If you want to know whether a closed path contains a point, you can just use `CGPathContainsPoint`. But if you want to know whether a point is *near* a path, you need to precisely define *near*, and you need to have code that tests for nearness. I think we've given you a simple way to define and test nearness using `CGPathCreateCopyByStrokingPath` and `CGPathContainsPoint` together. Why would you need to rewrite your entire program? – rob mayoff Mar 12 '13 at 18:37
4

The CGContextPathContainsPoint method requires a graphics context, which Apple's sample code gets from UIGraphicsGetCurrentContext. However, UIGraphicsGetCurrentContext only works inside -[UIView drawRect:] or after a call to a function that sets a UI graphics context, like UIGraphicsBeginImageContext.

You can perform your hit-testing without a graphics context by using CGPathCreateCopyByStrokingPath (which was added in iOS 5.0) and CGPathContainsPoint on the stroked copy:

static BOOL strokedPathContainsPoint(CGPathRef unstrokedPath,
    const CGAffineTransform *transform, CGFloat lineWidth,
    CGLineCap lineCap, CGLineJoin lineJoin, CGFloat miterLimit,
    CGPoint point, bool eoFill)
{
    CGPathRef strokedPath = CGPathCreateCopyByStrokingPath(unstrokedPath,
        transform, lineWidth, lineCap, lineJoin, miterLimit);
    BOOL doesContain = CGPathContainsPoint(strokedPath, NULL, point, eoFill);
    CGPathRelease(strokedPath);
    return doesContain;
}

You have to decide what line width and other stroking parameters you want to use. For example:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    CGPoint curPoint = [[touches anyObject] locationInView:self];
    for (int i = 0; i < [pathInfo count]; i++){
        NSArray *row = [[NSArray alloc] initWithArray:[pathInfo objectAtIndex:i]];
        UIBezierPath *path = [row objectAtIndex:0];

        NSLog(@"Path %@", path);
        if (strokedPathContainsPoint(path.CGPath, NULL, 10.0f, kCGLineCapRound,
            kCGLineJoinRound, 0, curPoint, path.usesEvenOddFillRule))
        {
            NSLog(@"YES");
        } else {
            NSLog(@"NO");
        }
    }
}

Note that CGPathCreateCopyByStrokingPath is probably somewhat expensive, so you might want to stroke your paths once, and save the stroked copies, instead of stroking them every time you need to test a point.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848