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 2 of 4 of this article. Click here to view the first page.

Detecting a Circle

“But, hold on a second,” you cry. “A tap does not a circle make!”

Well, if you wanna get all technical about it, a single point is a circle with a radius of 0. But that’s not what’s intended here; the user has to actually circle the image for the selection to count.

To find the circle, you’ll have to collect the points that the user moves his or her finger over and see if they form a circle.

This sounds like a perfect job for a collection.

Add the following instance variable to the top of the CircleGestureRecognizer class:

private var touchedPoints = [CGPoint]() // point history

You’ll use this to track the points the user touched.

Now add the following method to the CircleGestureRecognizer class:

override func touchesMoved(touches: Set<NSObject>!, withEvent event: UIEvent!) {
  super.touchesMoved(touches, withEvent: event)

  // 1
  if state == .Failed {
    return
  }

  // 2
  let window = view?.window
  if let touches = touches as? Set<UITouch>, loc = touches.first?.locationInView(window) {
    // 3
    touchedPoints.append(loc)
    // 4
    state = .Changed
  }
}

touchesMoved(_:withEvent:) fires whenever the user moves a finger after the initial touch event. Taking each numbered section in turn:

  1. Apple recommends you first check that the gesture hasn’t already failed; if it has, don’t continue to process the other touches. Touch events are buffered and processed serially in the event queue. If a the user moves the touch fast enough, there could be touches pending and processed after the gesture has already failed.
  2. To make the math easy, convert the tracked points to window coordinates. This makes it easier to track touches that don’t line up within any particular view, so the user can make a circle outside the bounds of the image, and have it still count towards selecting that image.
  3. Add the points to the array.
  4. Update the state to .Changed. This has the side effect of calling the target action as well.

.Changed is the next state to add to your state machine. The gesture recognizer should transition to .Changed every time the touches change; that is, whenever the finger is moved, added, or removed.

Here’s your new state machine with the .Changed state added:

state machine with .Changed added

Now that you have all the points, how are you going to figure out if the points form a circle?

all the points

Checking the Points

To start, add the following variables to the top of the class in CircleGestureRecognizer.swift:

var fitResult = CircleResult() // information about how circle-like is the path
var tolerance: CGFloat = 0.2 // circle wiggle room
var isCircle = false

These will help you determine if the points are within tolerance for a circle.

Update touchesEnded(_:withEvent:) so that it looks like the code below:

override func touchesEnded(touches: Set<NSObject>!, withEvent event: UIEvent!) {
  super.touchesEnded(touches, withEvent: event)

  // now that the user has stopped touching, figure out if the path was a circle
  fitResult = fitCircle(touchedPoints)

  isCircle = fitResult.error <= tolerance
  state = isCircle ? .Ended : .Failed
}

This cheats a little bit as it uses a pre-made circle detector. You can take a peek at CircleFit.swift now, but I'll describe its inner workings in just a bit. The main take-away is that the detector tries to fit the traced points to a circle. The error value is how far the path deviated from a true circle, and the tolerance is there because you can’t expect users to draw a perfect circle. If the error is within tolerance, the recognizer moves to the .Ended state; if the circle is out of tolerance then move to .Failed.

If you were to build and run right now, the game wouldn’t quite work because the gesture recognizer is still treating the gesture like a tap.

Go back to GameViewController.swift, and change circled(_:) as follows:

func circled(c: CircleGestureRecognizer) {
  if c.state == .Ended {
    findCircledView(c.fitResult.center)
  }
}

This uses the calculated center of the circle to figure out which view was circled, instead of just getting the last point touched.

Build and run your app; try your hand at the game — pun quite intended. It’s not easy to get the app to recognize your circle, is it? What’s remaining is to bridge the difference between mathematical theory and the real world of imprecise circles.

nowork

Drawing As You Go

Since it's tough to tell exactly what's going on, you'll draw the path the user traces with their finger. iOS already comes with most of what you need in Core Graphics.

Add the following to the instance variable declarations in CircleGestureRecognizer.swift:

var path = CGPathCreateMutable() // running CGPath - helps with drawing

This provides a mutable CGPath object for drawing the path.

Add the following to the bottom of touchesBegan(_:withEvent:):

let window = view?.window
if let touches = touches as? Set<UITouch>, loc = touches.first?.locationInView(window) {
  CGPathMoveToPoint(path, nil, loc.x, loc.y) // start the path
}

This makes sure the path starts out in the same place that the touches do.

Now add the following to touchesMoved(_:withEvent:), just below touchedPoints.append(loc) in the if let block at the bottom:

CGPathAddLineToPoint(path, nil, loc.x, loc.y)

Whenever the touch moves, you add the new point to the path by way of a line. Don't worry about the straight line part; since the points should be very close together, this will wind up looking quite smooth once you draw the path.

In order to see the path, it has to be drawn in the game’s view. There’s already a view in the hierarchy of CircleDrawView.

To show the path in this view, add the following to the bottom of circled(_:) in GameViewController.swift:

if c.state == .Began {
  circlerDrawer.clear()
}
if c.state == .Changed {
  circlerDrawer.updatePath(c.path)
}

This clears the view when a gesture starts, and draws the path as a yellow line that follows the user's finger.

Build and run your app; try drawing on the screen to see how it works:

Now draws the path

whoah

Cool! But did you notice anything funny when you drew a second or third circle?

iOS Simulator Screen Shot 1 Jun 2015 21.09.19

Even though you added a call to circlerDrawer.clear() when moving into the .Began state, it appears that each time a gesture is made, the previous ones are not cleared. That can only mean one thing: it's time for a new action in your gesture recognizer state machine: reset().