# An one finger rotation gesture recognizer

Last year we developed the Raumfeld iPhone App. The goal was to build something that could replace our native controller completely. The main design feature of the native controller is the massive volume knob. Of course we wanted to have something similar in our App, so the designer created the volume screen with a big knob, as shown in the right picture.

To create a volume control, we needed a way to detect if the user performs a rotation gesture on the knob image. There is no default UIGestureRecognizer that detects such a gesture, so we had to implement a custom gesture recognizer. It was surprisingly easy – to be honest, the hardest part was to do the math right.

To track the finger movement, we need to check for every touch event, whether it is within the defined area. The gesture is rotating, so there is a center point m, the radius a which defines the minimum distance from m and the radius b which defines the maximum distance from m. Finally we need to calculate the angle ? between the startpoint and the current finger position (relative to m).

For the first check, we calculate the distance d from the center and make sure, that a < d < b is true.

```/** Calculates the distance between point1 and point 2. */ CGFloat distanceBetweenPoints(CGPoint point1, CGPoint point2) { CGFloat dx = point1.x - point2.x; CGFloat dy = point1.y - point2.y; return sqrt(dx*dx + dy*dy); }```

The rotation angle is the angle between the two lines a and b. The arc tangent function atan() is the key:

```/** The method is a bit too generic - in our case both lines share the same start point. */ CGFloat angleBetweenLinesInDegrees(CGPoint beginLineA, CGPoint endLineA, CGPoint beginLineB, CGPoint endLineB) { CGFloat a = endLineA.x - beginLineA.x; CGFloat b = endLineA.y - beginLineA.y; CGFloat c = endLineB.x - beginLineB.x; CGFloat d = endLineB.y - beginLineB.y;   CGFloat atanA = atan2(a, b); CGFloat atanB = atan2(c, d);   // convert radiants to degrees return (atanA - atanB) * 180 / M_PI; }```

Ok, that was the hard part 🙂 To implement a custom gesture recognizer, let’s have a look at the UIGestureRecognizer API docs, especially the subclassing notes. We just need to overwrite five methods

```- (void)reset; - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;```

The key is `touchMoved:withEvent`: This method checks the distance of the touch event from the center point. If the touch is within the valid area, the angle between the start point and the current touch position is calculated. The result is sent to the delegate object of our class.

```- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event];   if (self.state == UIGestureRecognizerStateFailed) return;   CGPoint nowPoint = [[touches anyObject] locationInView: self.view]; CGPoint prevPoint = [[touches anyObject] previousLocationInView: self.view];   // make sure the new point is within the area CGFloat distance = distanceBetweenPoints(midPoint, nowPoint); if ( innerRadius < = distance && distance <= outerRadius) { // calculate rotation angle between two points CGFloat angle = angleBetweenLinesInDegrees(midPoint, prevPoint, midPoint, nowPoint);   // fix value, if the 12 o'clock position is between prevPoint and nowPoint if (angle > 180) { angle -= 360; } else if (angle < -180) { angle += 360; }   // sum up single steps cumulatedAngle += angle;   // call delegate if ([target respondsToSelector: @selector(rotation:)]) { [target rotation:angle]; } } else { // finger moved outside the area self.state = UIGestureRecognizerStateFailed; } }```

`target` needs to implement some kind of protocol to allow the gesture recognizer notification of movements:

```@protocol OneFingerRotationGestureRecognizerDelegate <nsobject> @optional /** A rotation gesture is in progress, the frist argument is the rotation-angle in degrees. */ - (void) rotation: (CGFloat) angle; /** The gesture is finished, the first argument is the total rotation-angle. */ - (void) finalAngle: (CGFloat) angle; @end </nsobject>```

And that’s the whole magic. To use the gesture recognizer, create an image of a rotating control and add a gesture recognizer to your view:

```// calculate center and radius of the control CGPoint midPoint = CGPointMake(image.frame.origin.x + image.frame.size.width / 2, image.frame.origin.y + image.frame.size.height / 2); CGFloat outRadius = image.frame.size.width / 2;   // outRadius / 3 is arbitrary, just choose something >> 0 to avoid strange // effects when touching the control near of it's center gestureRecognizer = [[OneFingerRotationGestureRecognizer alloc] initWithMidPoint: midPoint innerRadius: outRadius / 3 outerRadius: outRadius target: self]; [someView addGestureRecognizer: gestureRecognizer];```

As soon as a gesture is detected, the delegate method is called and you can rotate the image according to the angle:

```- (void) rotation: (CGFloat) angle { // calculate rotation angle imageAngle += angle; if (imageAngle > 360) imageAngle -= 360; else if (imageAngle < -360) imageAngle += 360;   // rotate image and update text field image.transform = CGAffineTransformMakeRotation(imageAngle * M_PI / 180); textDisplay.text = [NSString stringWithFormat: @"\u03b1 = %.2f", imageAngle]; }```

I’ve created a sample project on github, feel free to play with. Also make sure to read Ole Begemanns article about the UX details on gesture recognition.

## 29 Antworten auf „An one finger rotation gesture recognizer“

1. UJ sagt:

Hey, wenn ich gewusst hätte, dass du bei Raumfeld arbeitest, hätte ich mich natürlich dafür entschieden. So ist es leider nur ein sonos system geworden.
Bin aber sehr zufrieden 😉

Danke das ist ein sehr Hilfreicher Blog eintrag.
Somit weiß ich nun was man sich Holen muss.
Schaut auch sehr gut aus.

3. Lewis sagt:

Hi, could you explain how I could edit your code so when a finger rotation stops, the knob takes the original start position, I’m not sure how I would do that so it moves to that location at an even speed?

4. Lewis sagt:

^ a little like a steering wheel.

5. Michael Grumbach sagt:

Great solution for one finger rotation.
Thank you very much for sharing.
I’m a newbie and I need to access the angleInRadians value, calculate a new value from this, and use that new value to move a UIImageView across the screen.
The problem is that this is not in the ViewController and I don’t know how to get this value back to the ViewController.
If you could point me in the right direction, that would be great.

Best Regards,
Mike

6. @Mike: Either skip the conversion in angleBetweenLinesInDegrees, but make sure that you adopt touchesMoved:withEvent: to calculate with radiant values. Or you convert the value back to radiants in the rotation: method.

Your ViewController should implement the protocol to receive the rotation: calls. See the example source code at github, the view controller there is implementing the protocol and updates the textfield.

7. Artur sagt:

Hi, I want to change angle from 360′ to 270′ and stop it when it reaches 270 or 0 on a scroll. How can I do this?

Best Regards,
Artur

8. Gabriel sagt:

Danke !
That’s exactly what I was looking for.
As my „wheel“ view was nested in another view, I passed the parent view in the init and replaced all references to „self.view“ in touchesMoved with this. Works like a charm.

9. Gabriel sagt:

@Arthur: I guess you should change „touchesMoved:“ and replace:

// fix value, if the 12 o’clock position is between prevPoint and nowPoint
if (angle > 180)
{
angle -= 360;
}
else if (angle 270)
{
angle = 270;
}
else if (angle < 0)
{
angle = 0;
}

10. John sagt:

Hi. What if I have image in a frame and therefore image location is relative to the frame, not to the whole window

11. Artur sagt:

Hi, I have three uibutton on this view. They only work fine when I swipe on them. How can I use with this gesture recognizer buttons? Where should I make changes?

Artur

12. Artur sagt:

Hi one again. In my previous comment I want to ask how did you do this „close-x“ button to work as a normal button. My buttons on this controller work fine when I swipe on them.

13. Paul sagt:

OK, while I’m relatively new to Objective-C I am versed enough in programming to say that that’s an absolutely brilliant piece of code. If you could point me in the right direction however, how can I use this class across a collection of knobs? Say for example that I’ve got 3 UIView knobs and 3 text fields in a single viewcontroller – how can I get the class to respond to each of the knobs independently? I thought perhaps using a switch statement in the

if ([target respondsToSelector: @selector(rotation:)])
{
[target rotation:angle];
}

section of your implementation file, but I can’t seem to figure out how to return which knob is being dragged. Any help would be greatly appreciated.

14. Paul sagt:

OK, I figured out how to use the delegate across a multitude of knobs, now can anyone give me a clue how to change the values so that 0 isn’t at 12 o’clock? I’d like to get 0 at 7 o’clock and the high end at like 4 o’clock – similar to a volume knob on a radio.

15. Torben sagt:

Can i get the source code!

16. Arslan Khalid sagt:

Hi
its great tutorial about rotation object with single finger i was in trying to find such that tutorial thanks

17. Leon sagt:

I have used this control for 8 knobs in a row. There I found an error.

I solved this by replacing:

– (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{

CGPoint nowPoint = [[touches anyObject] locationInView: self.view];
CGPoint prevPoint = [[touches anyObject] previousLocationInView: self.view];

with:

CGPoint nowPoint = [[touches anyObject] locationInView: self.view.superview];
CGPoint prevPoint = [[touches anyObject] previousLocationInView: self.view.superview];

Otherwise, the touches locations where wrong.

18. Shashi Kant sagt:

Very nice post solved my issue

19. chipbk10 sagt:

It does not work on iOS7. I tried to implement your code from the scratch, but the image always fluctuate while rotating.

20. Niklas sagt:

Great article ! I made a VERY simple little app drive a „dial“, but purely based on swipe gesture speeds (split the wheel into for gesture zones and combined x and y speeds). The issue was to bind that to the actual „dot“ on the dial. This solution actually looks at the touch position so way way better and still very short. Thanks for sharing !! 😀

21. Michael sagt:

Any update for iphone 6 screen? seem it did not work on i6 and i6+

22. @Michael: how about sending a pull-request? 😉

23. Edgars sagt:

In Xcode 6, where new template has LaunchScreen.xib, this doesn’t work. Problem is in file OneFingerRotationGestureRecognizer.m, at line – CGPoint prevPoint = [[touches anyObject] previousLocationInView: self.view];
There are 2 errors showing. I can’t avoid them.
Interesting, that everything still works in old projects, which were made in previous versions of xcode, even if I take old project and include OneFingerRotationGestureRecognizer in it.
So error messages are:
1) No known instance method for selector ‚previousLocationInView:‘
2) Initializing ‚CGPoint‘ (aka ’struct CGPoint‘) with an expression of incompatible type ‚id‘
Can somebody tell me how to avoid it?

24. this is great work! Do you happen to have the swift version of this?

25. Salehi sagt:

how can i use this code to control two independent knob in a view

26. waleed jebreen sagt:

Please make a swift 3 version of this.

27. @ waleed jebreen: I’m happyly accepting pull-requests on github 😬

Kommentare sind geschlossen.