I wanted to learn more about custom gesture recognizers so I was reading the Ray Wenderlich tutorial, which I planned to modify in order to learn the details and what I can change easily to learn how each piece works, but it was written in a previous version of Swift. Swift updated most of the code and I was able to fix the rest manually except that I am having trouble getting the touch gestures to be drawn on the screen, and no shapes are recognized as circles, which I'm hoping both tie back to the same problem. The website and code snippet are as follows:
https://www.raywenderlich.com/104744/uigesturerecognizer-tutorial-creating-custom-recognizers
import UIKit
import UIKit.UIGestureRecognizerSubclass
class CircleGestureRecognizer: UIGestureRecognizer {
fileprivate var touchedPoints = [CGPoint]() // point history
var fitResult = CircleResult() // information about how circle-like is the path
var tolerance: CGFloat = 0.2 // circle wiggle room, lower is more circle like higher is oval or other
var isCircle = false
var path = CGMutablePath() // running CGPath - helps with drawing
override func touchesBegan(_ touches: (Set<UITouch>!), with event: UIEvent) {
if touches.count != 1 {
state = .failed
}
state = .began
let window = view?.window
if let touches = touches, let loc = touches.first?.location(in: window) {
//print("path 1 \(path.currentPoint)")
path.move(to: CGPoint(x: loc.x, y: loc.y)) // start the path
print("path 2 \(path.currentPoint)")
}
//super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: (Set<UITouch>!), with event: UIEvent) {
// 1
if state == .failed {
return
}
// 2
let window = view?.window
if let touches = touches, let loc = touches.first?.location(in: window) {
// 3
touchedPoints.append(loc)
print("path 3 \(path.currentPoint)")
path.move(to: CGPoint(x: loc.x, y: loc.y))
print("path 4 \(path.currentPoint)")
// 4
state = .changed
}
}
override func touchesEnded(_ touches: (Set<UITouch>!), with event: UIEvent) {
print("path 5 \(path.currentPoint)")
// now that the user has stopped touching, figure out if the path was a circle
fitResult = fitCircle(touchedPoints)
// make sure there are no points in the middle of the circle
let hasInside = anyPointsInTheMiddle()
let percentOverlap = calculateBoundingOverlap()
isCircle = fitResult.error <= tolerance && !hasInside && percentOverlap > (1-tolerance)
state = isCircle ? .ended : .failed
}
override func reset() {
//super.reset()
touchedPoints.removeAll(keepingCapacity: true)
path = CGMutablePath()
isCircle = false
state = .possible
}
fileprivate func anyPointsInTheMiddle() -> Bool {
// 1
let fitInnerRadius = fitResult.radius / sqrt(2) * tolerance
// 2
let innerBox = CGRect(
x: fitResult.center.x - fitInnerRadius,
y: fitResult.center.y - fitInnerRadius,
width: 2 * fitInnerRadius,
height: 2 * fitInnerRadius)
// 3
var hasInside = false
for point in touchedPoints {
if innerBox.contains(point) {
hasInside = true
break
}
}
//print(hasInside)
return hasInside
}
fileprivate func calculateBoundingOverlap() -> CGFloat {
// 1
let fitBoundingBox = CGRect(
x: fitResult.center.x - fitResult.radius,
y: fitResult.center.y - fitResult.radius,
width: 2 * fitResult.radius,
height: 2 * fitResult.radius)
let pathBoundingBox = path.boundingBox
// 2
let overlapRect = fitBoundingBox.intersection(pathBoundingBox)
// 3
let overlapRectArea = overlapRect.width * overlapRect.height
let circleBoxArea = fitBoundingBox.height * fitBoundingBox.width
let percentOverlap = overlapRectArea / circleBoxArea
print("Percent Overlap \(percentOverlap)")
print("pathBoundingBox \(pathBoundingBox)")
print("path 6 \(path.currentPoint)")
return percentOverlap
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled // forward the cancel state
}
}
As shown in the tutorial this bit of code is supposed to compare a bounding box for the path to a box that would fit a circle and compare the overlap, but when I print the pathBoundingBox is states: "pathBoundingBox (inf, inf, 0.0, 0.0)" which is probably why the percentOverlap is 0. I was thinking it was the path.move(to: loc) where loc is the first touch location but the documentation for move(to:) says "This method implicitly ends the current subpath (if any) and sets the current point to the value in the point parameter." so I'm struggling to figure out why the path.boundingBox is infinite...
That's not an infinite bounding box, it's just the opposite — a zero bounding box. The problem is that your
path
is empty.