Show Menu

Looking for an iOS Developer?

Submit your 30 day Job Listing for FREE

Any developer with aspirations of creating a killer app will almost certainly expand the UI beyond the standard offerings provided by Apple. While the standard API is functional and fairly complete, the intense competition in the App Store requires the user experience (UX) to be exceptional, highly attractive, and immediately intuitive. Even with beautiful graphics, the touch interface needs to be flawless and instinctive, which often times requires extending the standard touch gestures.

The standard Apple API class UIGestureRecognizer supports all the standard touch behaviors, including taps, pinch, rotation, swipe, pan, and press. To provide a gesture other than a built-in one requires the creation of a custom gesture recognizer. The task is not too difficult, and this article will briefly explain the process and then provide a detailed and fully-functional example with complete working code on GitHub .

Creating a Custom Gesture Recognizer

The main task in creating a custom gesture recognizer is subclassing UIGestureRecognizer. To provide the necessary and proper methods, a subclass must correctly implement the gesture state machine.

The Gesture Recognizer State Machine

Apple has greatly simplified the processing of touch events by implementing a state machine within the UIGestureRecognizer. The standard API gestures all use this state machine, and so should any custom gesture.

UIGestureRecognizer State

The diagram shows a very simple state machine, but it is powerful enough to represent any possible gesture. The seven potential states are:

Possible – This is the state before the screen is touched, and anything is possible.
Failed – This state is entered when a specific gesture fails. For example, a single-tap gesture fails if the screen is tapped twice.
Recognized – This state is entered when a discreet gesture succeeds.
Began – This state is entered upon the very first touch, just as the gesture is beginning.
Changed – This state is entered when and as the gesture changes.
Ended – This state is the normal completion state for a continuous gesture.
Cancelled – This state is entered if the gesture is not completed.

At any point, a subclass can explicitly set the state by calling the UIGestureRecognizer method setState. Only a subclass of UIGestureRecognizer may call this method, and then only if the header file has been imported by the subclass.

Methods To Interact with the State Machine

In order to successfully recognize a custom gesture, the subclass must navigate the state machine, responding appropriately to touch events. The following events can be called on the subclass, and it should handle them in the manner consistent with the desired gesture.


- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event

This event is called at the very beginning of the gesture, and represents the state change from Possible to Began. This method can be used to register the beginning, as well as to check for an invalid start, such as multiple fingers touching.


- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event

This event is called every single time a touch moves, regardless of the number of touches present. This event represents the Changed state, and can be entered many times during the gesture. In this method various calculations as to direction and velocity can be made, as well as checks on the validity of the movement.


- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event

This event is called at the successful completion of the gesture. It is normally used to inform the view controller, but may also be employed to perform additional clean-up or post-gesture actions. The state should be set to Ended or Recognized, depending on whether the gesture is continuous or discrete, respectively


- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event

This event is called if the gesture is interrupted before completion. The state should be set to Canceled or Failed, depending on whether the gesture is continuous or discrete, respectively.

Create the subclass as a delegating class

The custom gesture recognizer class needs to be created as a delegating class so that the view may register as the class’ delegate in order to receive notice of actions. Implement the – (id)initWithTarget:(id)target action:(SEL)action method for the subclass so that the view controller can register itself and the method to be called as an action arising out of the gesture processing.

Adding the gesture recognizer to the view

Any gesture recognizer, whether standard or custom, operates on a view. That view may be the main view, or it may be a subview, but it must be a view. There are five basic steps required to add a gesture recognizer to the view:

  • Declare that the view class adopts the protocol of the delegating class in the class definition.
    For example, @interface MyViewController : UIViewController
  • Initialize the gesture recognizer using the – (id)initWithTarget:(id)target action:(SEL)action method, with self as the target and the method handling the result as the action.
  • Set the delegate of the gesture recognizer to self.
  • Use the addGestureRecognizer method to register the gesture recognizer on the view.
  • Implement an action method, as referenced in the – (id)initWithTarget:(id)target action:(SEL)action initialization.

Implementing an action method

The second argument to the gesture recognizer initialization method is a selector (SEL) reference to an action method. This action method will be called by the gesture recognizer when the state has changed, ended, or been recognized. The purpose of this method is to perform the action which is desired by the gesture. The method should receive at least one argument of type (id), representing the sender of the action (which in this case is the gesture recognizer itself).

In addition to performing some desired action, the action method may also check the state of the gesture recognizer and act accordingly

A Working Demo

A common user interface is a circular knob. Unfortunately, the stock API only provides the UIRotationGestureRecognizer as a means to turn the knob which would require the user to place two fingers on the screen and then perform a rotation gesture. This standard gesture is not very intuitive, and would be very awkward for this purpose. A much better gesture would be to simply allow the user to use one finger to rotate the knob. This demo details the code to create just such a custom gesture recognizer and names it SwirlGestureRecognizer.

For the demo, a simple colorful knob is allowed to rotate 360° with a default starting point of 0° straight up. The graphic is 320 pixels square, or 640 pixels for the retina @2x display. Having a symmetrical graphic makes rotation easy, but of course in practice it could be anything of any size, with any starting point and limits.

ios app knob rotation

The SwirlGestureRecognizer class

A separate class called SwirlGestureRecognizer is created as a subclass of UIGestureRecognizer and also as a delegating class.

SwirlGestureRecognizer.h

First import the gesture recognizer subclass so that the class has access to the proper methods and can set the state:


#import 

In order to make this a delegating class, provide the delegate protocol. In this case, the delegate protocol will inherit all the methods from the UIGestureRecognizerDelegate and have no need to add any methods.


@protocol SwirlGestureRecognizerDelegate 

@end

The interface declares this class as a subclass of UIGestureRecognizer and defines two public properties:


@interface SwirlGestureRecognizer : UIGestureRecognizer

@property CGFloat currentAngle;
@property CGFloat previousAngle;

@end

SwirlGestureRecognizer.m


#import "SwirlGestureRecognizer.h"

@interface SwirlGestureRecognizer ()

@property (strong, nonatomic) id target;
@property (nonatomic) SEL action;

@end

@implementation SwirlGestureRecognizer

- (id)initWithTarget:(id)target action:(SEL)action {
    
    if (self = [super initWithTarget:target action:action]) {
        self.target = target;
        self.action = action;
    }
    
    return self;
}

For this demo, the only subclass methods which need to be overridden are the four touches methods.


- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    
    if (touches.count > 1) {
        self.state = UIGestureRecognizerStateFailed;
        return;
    }
}

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    
    UITouch *touch = [touches anyObject];
    
    self.currentAngle = [self getTouchAngle:[touch locationInView:touch.view]];
    self.previousAngle = [self getTouchAngle:[touch previousLocationInView:touch.view]];
    
    if ([self.target respondsToSelector:self.action]) {
        [self.target performSelector:self.action withObject:self];

    }
}

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    [super setState:UIGestureRecognizerStateEnded];
}

- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    [super setState:UIGestureRecognizerStateCancelled];
}

The method getTouchAngle: determines the direction the user is dragging her finger by calculating the angle of the finger position relative to the center of the view. In the touchesMoved method above, getTouchAngle: is called on the previous touch and the current touch. In this way, the previous angle and the current angle can be compared to determine if the user is dragging her finger clockwise or counterclockwise.


- (float)getTouchAngle:(CGPoint)touch {
    
    // Translate into cartesian space with origin at the center of a 320-pixel square
    float x = touch.x - 160;
    float y = -(touch.y - 160);
    
    // Take care not to divide by zero!
    if (y == 0) {
        if (x > 0) {
            return M_PI_2;
        }
        else {
            return 3 * M_PI_2;
        }
    }
    
    float arctan = atanf(x/y);
    
    // Figure out which quadrant we're in
    
    // Quadrant I
    if ((x >= 0) && (y > 0)) {
        return arctan;
    }
    // Quadrant II
    else if ((x < 0) && (y > 0)) {
        return arctan + 2 * M_PI;
    }
    // Quadrant III
    else if ((x <= 0) && (y < 0)) {
        return arctan + M_PI;
    }
    // Quadrant IV
    else if ((x > 0) && (y < 0)) {
        return arctan + M_PI;
    }
    
    return -1;
}

@end

That is all there is to creating the custom gesture recognizer class! Obviously some gestures will require more or less code to implement, but this pattern applies to any custom gesture. Now the gesture can be used in exactly the same manner as standard Apple gestures.

All that’s left to is use the new class in the view controller.

Using a Gesture Recognizer in a View

The demo code implements the five basic steps in the view controller.

1. Declare the view class adopting the protocol of the SwirlGestureRecognizerDelegate

In the ViewController.h file, the interface is declared to adopt both the SwirlGestureRecognizerDelegate and also the UIGestureRecognizerDelegate since the demo will also recognize a standard tap gesture.


@interface CAYViewController : UIViewController 

2. Initialize the gesture recognizer


self.swirlGestureRecognizer = [[SwirlGestureRecognizer alloc] 
    initWithTarget:self action:@selector(rotationAction:)];

3. Set the delegate of the gesture recognizer to self.


[self.swirlGestureRecognizer setDelegate:self];

4. Add the gesture recognizer to the view

For this demo, a subview named controlsView is layered over the main view. This allows the gesture to only be recognized on that view. It is not absolutely necessary, but it does give better user feedback when only those gestures in the knob area will rotate the knob.


[self.controlsView addGestureRecognizer:self.swirlGestureRecognizer];

Implement a rotation action method

The following method defines the action which executes when the gesture recognizer calls the selector. It rotates the knob graphic using a CGAffineTransform, and updates the position text. In order to determine which direction and how much to rotate the knob, all that is needed is a simple subtraction of the previous touch angle from the current touch angle. The method also keeps a running track of the current bearing so that the position text can be updated.


- (void)rotationAction:(id)sender {
    
    if([(SwirlGestureRecognizer*)sender state] == UIGestureRecognizerStateEnded) {
        return;
    }
    
    CGFloat direction = ((SwirlGestureRecognizer*)sender).currentAngle 
        - ((SwirlGestureRecognizer*)sender).previousAngle;
    
    bearing += 180 * direction / M_PI;
    
    if (bearing < -0.5) {
        bearing += 360;
    }
    else if (bearing > 359.5) {
        bearing -= 360;
    }
    
    CGAffineTransform knobTransform = self.knob.transform;
    
    CGAffineTransform newKnobTransform = CGAffineTransformRotate(knobTransform, direction);

    [self.knob setTransform:newKnobTransform];
    
    self.position.text = [NSString stringWithFormat:@"%d°", (int)lroundf(bearing)];
}

With that small amount of code, a completely new gesture will now function. With what has been detailed so far, the user will be able to rotate the knob by simply touching and dragging. The demo goes one better though, by adding the ability to reset the knob back to zero with a double-tap.

Adding more than one gesture recognizer

There are many times when a single view will need to respond to more than one gesture. For this example, a double-tap gesture has been added which will smoothly return the knob to the zero position. Any number of gestures may be added to a view, but there are a few details to keep in mind:

  • The gestures must contain at least one distinctly different action.
  • Other gestures must fail in order for one to succeed or be recognized.

To add a double-tap gesture recognizer, the following code is added. Notice that it is nearly identical to the code used to add the swirl gesture.


self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] 
    initWithTarget:self action:@selector(resetToZero:)];
    
[self.tapGestureRecognizer setDelegate:self];
    
// The distinctly different action is the number of taps required
self.tapGestureRecognizer.numberOfTapsRequired = 2;
    
[self.controlsView addGestureRecognizer:self.tapGestureRecognizer];

// The tap gesture is required to fail in order for the swirl gesture to succeed  
[self.swirlGestureRecognizer requireGestureRecognizerToFail:self.tapGestureRecognizer];

Anything is possible

Armed with the power of the gesture recognizer state machine and the mechanism to add any number of gestures, there is almost nothing conceivable that couldn’t be implemented. However, just because it can be done does not mean that it should be done. Alway keep in mind the user experience (UX) when designing new gestures. The simplest and most intuitive gesture is almost always the best gesture.
[socialRansom link="https://github.com/CayuseConcepts/KnobRotation/archive/master.zip"]

having issues?

We have a Questions and Answer section where you can ask your iOS Development questions to thousands of iOS Developers.

Ask Question

FREE Download!

Get your FREE Swift 2 Cheat Sheet and quick reference guide PDF download when you sign up to SwiftMonthly


Sharing is caring

If you enjoyed this tutorial, please help us and others by sharing using one of the social media buttons below.


Written by:

Scott Erholm is a developer with over 18 years experience in many different software engineering and architecture domains. His passion is data manipulation and visualization from raw collection to user interfaces on Web, iOS and Android platforms.

Comments

comments