Search code examples
iosswiftuigesturerecognizercgpath

Swift 3 Gesture Recognizers path.boundingBox is infinite


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...


Solution

  • That's not an infinite bounding box, it's just the opposite — a zero bounding box. The problem is that your path is empty.