Perhaps a simpler solution, at least for perfect shapes: assuming you know the shape you are looking for, you can create the path of that shape within the bounds of your user's UIBezierPath, and then perform a hit test against a stroked version of that path.
For instance, here's a Swift 5 method that detects if a path resembles an oval. I also threw in a method I adapted from another post here to extract all the elements of a UIBezierPath, except a subpath's closure:
extension UIBezierPath {
/// Indicates whether a manually drawn UIBézierPath resembles an oval.
func resemblesOval() -> Bool {
// Get all the path points
let pathPoints = self.getPathElements().map({$0.point})
// Create the path of the perfect oval of the same bounds to perform a hit test against
let perfectOvalPath = UIBezierPath(ovalIn: self.bounds)
// Choose a hit test ribbon that is a quarter of the average of the bounds' width and height
let hitWidth = (self.bounds.width + self.bounds.height)/2 * 0.25
// Stroke the path of the desired perfect oval to create hit test criteria
let hitPath = perfectOvalPath.cgPath.copy(strokingWithWidth: hitWidth,
lineCap: .round,
lineJoin: .miter,
miterLimit: 0)
// Create an array to collect points that succeed in the hit test
var validPathPoints = [CGPoint]()
// Hit test for each point of our tested path
for point in pathPoints {
guard point != nil else { continue }
if hitPath.contains(point!) {
validPathPoints.append(point!)
}
}
// If 90% or more were successful, then it is an oval
if 10 * validPathPoints.count >= 9 * pathPoints.count {
return true
}
return false
}
/// An enum containing possible UIBezierPath element types. Use in conjunction with the `getPathElements` method.
enum PathElementType {
case move
case addLine
case addQuadCurve
case addCurve
}
/// Extracts all of the path elements, their points and their control points. Expected returned types belong to the enum `PathElementType`.
func getPathElements() -> [(type: PathElementType?, point: CGPoint?, controlPoint: CGPoint?, controlPoint1: CGPoint?, controlPoint2: CGPoint?)] {
let initialPath = UIBezierPath(cgPath: self.cgPath)
var bezierPoints = NSMutableArray()
initialPath.cgPath.apply(info: &bezierPoints, function: { info, element in
guard let resultingPoints = info?.assumingMemoryBound(to: NSMutableArray.self) else {
return
}
let points = element.pointee.points
let type = element.pointee.type
switch type {
case .moveToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add(NSString("move"))
case .addLineToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add(NSString("addLine"))
case .addQuadCurveToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add([NSNumber(value: Float(points[1].x)), NSNumber(value: Float(points[1].y))])
resultingPoints.pointee.add(NSString("addQuadCurve"))
case .addCurveToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add([NSNumber(value: Float(points[1].x)), NSNumber(value: Float(points[1].y))])
resultingPoints.pointee.add([NSNumber(value: Float(points[2].x)), NSNumber(value: Float(points[2].y))])
resultingPoints.pointee.add(NSString("addCurve"))
case .closeSubpath:
break
@unknown default:
break
}
})
let elementsTypes : [String] = bezierPoints.compactMap { $0 as? String }
let elementsCGFloats : [[CGFloat]] = bezierPoints.compactMap { $0 as? [CGFloat] }
var elementsCGPoints : [CGPoint] = elementsCGFloats.map { CGPoint(x: $0[0], y: $0[1]) }
var returnValue : [(type: PathElementType?, point: CGPoint?, controlPoint: CGPoint?, controlPoint1: CGPoint?, controlPoint2: CGPoint?)] = []
for i in 0..<elementsTypes.count {
switch elementsTypes[i] {
case "move":
returnValue.append((type: .move, point: elementsCGPoints.removeFirst(), controlPoint: nil, controlPoint1: nil, controlPoint2: nil))
case "addLine":
returnValue.append((type: .addLine, point: elementsCGPoints.removeFirst(), controlPoint: nil, controlPoint1: nil, controlPoint2: nil))
case "addQuadCurve":
let controlPoint = elementsCGPoints.removeFirst()
returnValue.append((type: .addQuadCurve, point: elementsCGPoints.removeFirst(), controlPoint: controlPoint, controlPoint1: nil, controlPoint2: nil))
case "addCurve":
let controlPoint1 = elementsCGPoints.removeFirst()
let controlPoint2 = elementsCGPoints.removeFirst()
returnValue.append((type: .addCurve, point: elementsCGPoints.removeFirst(), controlPoint: nil, controlPoint1: controlPoint1, controlPoint2: controlPoint2))
default:
returnValue.append((type: nil, point: nil, controlPoint: nil, controlPoint1: nil, controlPoint2: nil))
}
}
return returnValue
}
}