Hit detection when drawing lines in iOS

Viewed 3803

I would like to allow the user to draw curves in such a way that no line can cross another line or even itself. Drawing the curves is no problem, and I even found that I can create a path that is closed and still pretty line-like by tracing the nodes of the line forwards and back and then closing the path.

Unfortunately, iOS only provides a test for whether a point is contained in a closed path (containsPoint: and CGPathContainsPoint). Unfortunately, a user can pretty easily move their finger fast enough that the touch points land on both sides of an existing path without actually being contained by that path, so testing the touch points is pretty pointless.

I can't find any "intersection" of paths method.

Any other thoughts on how to accomplish this task?

2 Answers

Swift 4, answer is based on CGPath Hit Testing - Ole Begemann (2012)

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 tap target 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 bezierPaths = [UIBezierPath]()   // containing all lines already drawn
var tappedPaths = [CAShapeLayer]()

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

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

fileprivate func tapTarget(for path: UIBezierPath) -> UIBezierPath {

    let targetPath = path.copy(strokingWithWidth: path.lineWidth, lineCap: path..lineCapStyle, lineJoin: path..lineJoinStyle, miterLimit: path.miterLimit)

    return UIBezierPath.init(cgPath: targetPath)
}
Related