Search code examples
iphoneobjective-ccore-animationcalayer

hitTest overlapping CALayers


I have a UIView that contains a drawing that I've made using CALayers added as sublayers. It is a red square with a blue triangle centered inside. I am able to determine which shape has been touched using the following code:

CGPoint location = [gesture locationInView:self.view];
CALayer* layerThatWasTapped = [self.view.layer hitTest:location];
NSLog(@"Master Tap Location: %@", NSStringFromCGPoint(location));
NSLog(@"Tapped Layer Name: %@", layerThatWasTapped.name);
NSLog(@"Tapped Layer Parent: %@", layerThatWasTapped.superlayer.name);

int counter = layerThatWasTapped.superlayer.sublayers.count;
NSArray * subs =  layerThatWasTapped.superlayer.sublayers;

//Loop through all sublayers of the picture
for (int i=0; i<counter; i++) {
CALayer *layer = [subs objectAtIndex:i];
CAShapeLayer* loopLayer = (CAShapeLayer*)layerThatWasTapped.modelLayer;
CGPathRef loopPath = loopLayer.path;
CGPoint loopLoc = [gesture locationInView:cPage];        
loopLoc = [self.view.layer convertPoint:loopLoc toLayer:layer];
NSLog(@"loopLoc Tap Location: %@", NSStringFromCGPoint(loopLoc));

//determine if hit is on a layer
if (CGPathContainsPoint(loopPath, NULL, loopLoc, YES)) { 
NSLog(@"Layer %i Name: %@ Hit",i, layer.name);
} else {
NSLog(@"Layer %i Name: %@ No Hit",i, layer.name);
}
}

My problem lies with areas where the bounds of the triangle overlap the square. This results in the triangle registering the hit even when the hit is outside of the triangles path. This is a simplified example (I may have many overlapping shapes stacked in the view) Is there a way to loop through all of the sublayers and hittest each one to see if it lies under the tapped point? OR Is there a way to have the bounds of my layers match their paths so the hit occurs only on a visible area?


Solution

  • Since you're using CAShapeLayer, this is pretty easy. Make a subclass of CAShapeLayer and override its containsPoint: method, like this:

    @implementation MyShapeLayer
    
    - (BOOL)containsPoint:(CGPoint)p
    {
        return CGPathContainsPoint(self.path, NULL, p, false);
    }
    
    @end
    

    Make sure that wherever you were allocating a CAShapeLayer, you change it to allocate a MyShapeLayer instead:

    CAShapeLayer *triangle = [MyShapeLayer layer];  // this way
    CAShapeLayer *triangle = [[MyShapeLayer alloc] init]; // or this way
    

    Finally, keep in mind that when calling -[CALayer hitTest:], you need to pass in a point in the superlayer's coordinate space:

    CGPoint location = [gesture locationInView:self.view];
    CALayer *myLayer = self.view.layer;
    location = [myLayer.superlayer convertPoint:location fromLayer:myLayer];
    CALayer* layerThatWasTapped = [myLayer hitTest:location];