2

I have a CAShapeLayer for which I have marked the fill color as clear.

When I tap on the line it does not always detect if the CAShapeLayer cgpath contains the tap point. My code is as follows:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        guard let point = touch?.location(in: self) else { return }

        for sublayer in self.layer.sublayers! {
            if let l = sublayer as? CAShapeLayer {
               if let path = l.path, path.contains(point) {
                    print("Tap detected")
               }
            }
         }
}

On some occasions it detects it if I really click on the center on the line. enter image description here

So I thought of making the line very fat from 6 to 45. Still it did not work. Then I thought of making the fill as gray after this now when i tap on the fill gray color it always detects the tap. I am really confused why it detects tap on fill or very center of the line why not on the whole thickness of line.

enter image description here

caseynolan
  • 1,236
  • 1
  • 9
  • 11
Rahul
  • 300
  • 2
  • 12

2 Answers2

3

Swift 4, answer is based on explanation and link to CGPath Hit Testing - Ole Begemann (2012) by caseynolan in other comment:

From Ole Begemann blog:

contains(point: CGPoint)

This function is helpful if you want to hit test on the entire region the path covers. As such, contains(point: CGPoint) doesn’t work with unclosed paths because those don’t have an interior that would be filled.

copy(strokingWithWidth lineWidth: CGFloat, lineCap: CGLineCap, lineJoin: CGLineJoin, miterLimit: CGFloat, transform: CGAffineTransform = default) -> CGPath

This function creates a mirroring tapTarget object that only covers the stroked area of the path. When the user taps on the screen, we iterate over the tap targets rather than the actual shapes.


My solution in code

I use a UITapGestureRecognizer linked to the function tap():

var tappedLayers = [CAShapeLayer]()

@IBAction func tap(_ sender: UITapGestureRecognizer) {        
    let point = sender.location(in: imageView)

    guard let sublayers = imageView.layer.sublayers as? [CAShapeLayer] else {
        return
    }

    for layer in sublayers {
        // create tapTarget for path
        if let target = tapTarget(for: layer) {
            if target.contains(point) {
                tappedLayers.append(layer)
            }
        }
    }
}

fileprivate func tapTarget(for layer: CAShapeLayer) -> UIBezierPath? {
    guard let path = layer.path else {
        return nil
    }

    let targetPath = path.copy(strokingWithWidth: layer.lineWidth, lineCap: CGLineCap.round, lineJoin: CGLineJoin.round, miterLimit: layer.miterLimit)

    return UIBezierPath.init(cgPath: targetPath)
}
Leontien
  • 612
  • 5
  • 22
0

I think the problem is that CGPath.contains() doesn't operate the way you expect it to.

From Apple's CGPath docs:

Discussion

A point is contained in a path if it would be inside the painted region when the path is filled.

So the method isn't actually checking if you're intersecting a drawn line, it's checking if you're intersecting the shape (even if you're not explicitly filling the path).

Some basic experiments show that the method returns true if the supplied point:

  1. Sits exactly in the middle of a line/path (not including the line's drawn outline from the CAShapeLayer's lineWidth) or
  2. Is in the middle of where the path would create a solid shape (as if it had been closed and filled).

You might find some workarounds here on Stack Overflow (it seems that many others have had the same problem before). E.g. Hit detection when drawing lines in iOS.

You might also find this blog post useful: CGPath Hit Testing - Ole Begemann.

caseynolan
  • 1,236
  • 1
  • 9
  • 11