UIGestureRecognizer Tutorial: Creating Custom Recognizers

Learn how to detect circles in this custom UIGestureRecognizer tutorial. You’ll even learn how the math works! By Michael Katz.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

Handling Small Arcs

Now that you've handled the round, non-circular shapes, what about those pesky short arcs that look like they're part of a huge circle? If you look at the debug drawing, the size discrepancy between the path (black box) and the fit circle is huge:

small_arc

Paths that you want to recognize as a circle should at least approximate the size of the circle itself:

matching_circle

Fixing this should be as easy as comparing the size of the path against the size of the fit circle.

Add the following helper method to CircleGestureRecognizer.swift:

private 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 = CGPathGetBoundingBox(path)

  // 2
  let overlapRect = fitBoundingBox.rectByIntersecting(pathBoundingBox)

  // 3
  let overlapRectArea = overlapRect.width * overlapRect.height
  let circleBoxArea = fitBoundingBox.height * fitBoundingBox.width

  let percentOverlap = overlapRectArea / circleBoxArea
  return percentOverlap
}

This calculates how much the user’s path overlaps the fit circle:

  1. Find the bounding box of the circle fit and the user’s path. This uses CGPathGetBoundingBox to handle the tricky math, since the touch points were also captured as part of the CGMutablePath path variable.
  2. Calculate the rectangle where the two paths overlap using the rectByIntersecting method on CGRect
  3. Figure out what percentage the two bounding boxes overlap as a percentage of area. This percentage will be in the 80%-100% for a good circle gesture. In the case of the short arc shape, it will be very, very tiny!

Next, modify the isCircle check in touchesEnded(_:withEvent:) as follows:

let percentOverlap = calculateBoundingOverlap()
isCircle = fitResult.error <= tolerance && !hasInside && percentOverlap > (1-tolerance)

Build and run your app again; only reasonable circles should pass the test. Do your worst to fool it! :]

detect_small_arc_as_bad

awhyeah

Handling the Cancelled State

Did you notice the check for .Cancelled above in the debug drawing section? Touches are cancelled when a system alert comes up, or the gesture recognizer is explicitly cancelled through a delegate or by disabling it mid-touch. There's not much to be done for the circle recognizer other than to update the state machine. Add the following method to CircleGestureRecognizer.swift:

override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
  super.touchesCancelled(touches, withEvent: event)
  state = .Cancelled // forward the cancel state
}

This simply sets the state to .Cancelled when the touches are cancelled.

Handling Other Touches

With the game running, tap the New Set. Notice anything? That’s right, the button doesn’t work! That’s because the gesture recognizer is sucking up all the taps!

no_button_worky

There are a few ways to get the gesture recognizer to interact properly with the other controls. The primary way is to override the default behavior by using a UIGestureRecognizerDelegate.

Open GameViewController.swift. In viewDidLoad(_:) set the delegate of the gesture recognizer to self:

circleRecognizer.delegate = self

Now add the following extension at the bottom of the file, to implement the delegate method:

extension GameViewController: UIGestureRecognizerDelegate {
  func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
    // allow button press
    return !(touch.view is UIButton)
  }
}

This prevents the gesture recognizer from recognizing touches over a button; this lets the touch proceed down to the button itself. There are several delegate methods, and these can be used to customize where and how a gesture recognizer works in the view hierarchy.

Build and run your app again; tap the button and it should work properly now.

Spit-Shining the Game

All that's left is to clean up the interaction and make this a well-polished game.

First, you need to prevent the user from interacting with the view after an image has been circled. Otherwise, the path will continue to update while waiting for the new set of images.

Open GameViewController.swift. Add the following code to the bottom of selectImageViewAtIndex(_:):

circleRecognizer.enabled = false

Now re-enable your gesture recognizer at the bottom of startNewSet(_:), so the next round can proceed:

circleRecognizer.enabled = true

Next, add the following to the .Began clause in circled(_:):

if c.state == .Began {
  circlerDrawer.clear()
  goToNextTimer?.invalidate()
}

This adds a timer to automatically clear the path after a short delay so that the user is encouraged to try again.

Also in circled(_:), add the following code to the final state check:

if c.state == .Ended || c.state == .Failed || c.state == .Cancelled {
  circlerDrawer.updateFit(c.fitResult, madeCircle: c.isCircle)
  goToNextTimer = NSTimer.scheduledTimerWithTimeInterval(afterGuessTimeout, target: self, selector: "timerFired:", userInfo: nil, repeats: false)
}

This sets up a timer to fire a short time after the gesture recogniser either ends, fails or is cancelled.

Finally, add the following method to GameViewController:

func timerFired(timer: NSTimer) {
  circlerDrawer.clear()
}

This clears the circle after the timer fires, so that the user is tempted to draw another to have another attempt.

Build and run your app; If the gesture doesn’t approximate a circle, you'll see that the path clears automatically after a short delay.

Where to Go From Here?

You can download the completed project from this tutorial here.

You’ve built a simple, yet powerful circle gesture recognizer for your game. You can extend these concepts further to recognize other drawn shapes, or even customize the circle fit algorithm to fit other needs.

For more details, check out Apple’s documentation on Gesture Recognizers.

If you have any questions or comments about this tutorial, feel free to join the forum discussion below!