How To Create a Rotating Wheel Control with UIKit

This is a post whew you will learn how to build custom Rotating Wheel Control with UIKit, written by iOS Tutorial Team member Cesare Rocchi, a UX designer and developer specializing in web and mobile applications. There may be times you’ve downloaded a cool application with a new kind of user interface component, and you’ve […] By Cesare Rocchi.

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

Animating the Selection Centering

The last step is to implement the centering of the current sector. Let's revisit what that means.

When the user lifts their finger from the screen you have to calculate x, the current value of radians, and then determine the sector for that value. Then you have to calculate the difference between x and the midpoint of the sector and use it to build an affine transform.

First add a new property to SMRotaryWheel.h to store the current sector:

@property int currentSector;

Then, synthesize the new property in SMRotaryWheel.m:

@synthesize currentSector;

To handle the finger-lifted event, you need to override endTrackingWithTouch:withEvent: (touchesEnded:withEvent: is the event you need if you extend UIView).

In SMRotaryWheel.m, add the following code right below the continueTrackingWithTouch:withEvent: method to handle an odd number of sectors:

- (void)endTrackingWithTouch:(UITouch*)touch withEvent:(UIEvent*)event
{
    // 1 - Get current container rotation in radians
    CGFloat radians = atan2f(container.transform.b, container.transform.a);
    // 2 - Initialize new value
    CGFloat newVal = 0.0;
    // 3 - Iterate through all the sectors
    for (SMSector *s in sectors) {
        // 4 - See if the current sector contains the radian value
        if (radians > s.minValue && radians < s.maxValue) {
            // 5 - Set new value 
            newVal = radians - s.midValue;
            // 6 - Get sector number
            currentSector = s.sector;
			break;
        }
    }
    // 7 - Set up animation for final rotation
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.2];
    CGAffineTransform t = CGAffineTransformRotate(container.transform, -newVal);
    container.transform = t;
    [UIView commitAnimations];
}

The method is pretty simple: it calculates the current value of radians and compares it to min and max values to find the right sector. Then it finds the difference and builds a new CGAffineTransform. To make the effect look natural, the setting of the rotation is wrapped in an animation lasting 0.2 seconds.

Switch the app back to creating 3 sectors again by modifying section #2 of viewDidLoad in SMViewController.m, run, compile and ... ta da! It works. Grab the wheel and drag it as you wish. You’ll see that it settles on the right sector when you stop dragging and lift your finger up.

The code in its current form will work for all wheels with an odd number of sectors. To account for an even number of sectors, you have to rework the for loop (section #3) to check for the anomalous case, in which the min point is positive and the max is negative. Replace sections #4, #5, and #6 in endTrackingWithTouch:withEvent: with the following:

        // 4 - Check for anomaly (occurs with even number of sectors)
        if (s.minValue > 0 && s.maxValue < 0) {
            if (s.maxValue > radians || s.minValue < radians) {
                // 5 - Find the quadrant (positive or negative)
                if (radians > 0) {
                    newVal = radians - M_PI;
                } else {
                    newVal = M_PI + radians;                    
                }
                currentSector = s.sector;
            }
        }
        // 6 - All non-anomalous cases
        else if (radians > s.minValue && radians < s.maxValue) {
            newVal = radians - s.midValue;
            currentSector = s.sector;
        }

Compile, run and feel free to experiment by changing the number of sectors!

Adding Protocol Notifications

All the heavy lifting is behind us and it's all downhill from now on!

Remember the SMRotaryProtocol you defined at the very beginning? If you check initWithFrame: for SMRotaryWheel, you'll see that we already set the delegate. So, why don’t you add a label to the view controller to show the selected sector?

In SMViewController.h replace the existing code with the following:

#import "SMRotaryProtocol.h"

@interface SMViewController : UIViewController<SMRotaryProtocol>

@property (nonatomic, strong) UILabel *sectorLabel;

@end

Synthesize the new property in SMViewController.m:

@synthesize sectorLabel;

Then, replace viewDidLoad with the following:

- (void)viewDidLoad {
    // 1 - Call super method
    [super viewDidLoad];
    // 2 - Create sector label
	sectorLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 350, 120, 30)];
	sectorLabel.textAlignment = UITextAlignmentCenter;
	[self.view addSubview:sectorLabel];
    // 3 - Set up rotary wheel
    SMRotaryWheel *wheel = [[SMRotaryWheel alloc] initWithFrame:CGRectMake(0, 0, 200, 200)  
                                                    andDelegate:self 
                                                   withSections:12];
    wheel.center = CGPointMake(160, 240);
    // 4 - Add wheel to view
    [self.view addSubview:wheel];
}

The only method defined for the protocol can be added to the end of the file (before the @end) as follows:

- (void) wheelDidChangeValue:(NSString *)newValue {
    self.sectorLabel.text = newValue;
}

Now switch to SMRotaryWheel.m and add the following line after section #3:

        self.currentSector = 0;

Then add a call to the protocol method at the end of drawWheel:.

    // 9 - Call protocol method
    [self.delegate wheelDidChangeValue:[NSString stringWithFormat:@"value is %i", self.currentSector]];

Add the very same piece of code at the end of endTrackingWithTouch:withEvent: as well.

Compile and run. See? The label gets updated each time a sector is settled.

Cool! Now I know you want to make the component more attractive by adding some graphics.

Adding Graphics

Vicki Wenderlich has been very kind to provide the assets for our component. Here is the complete set.

Go ahead and download the graphics (instead of the single image above) and add them to your project. Once you’ve imported them, add two new static properties to SMRotaryWheel.m (above the @implementation statement).

static float minAlphavalue = 0.6;
static float maxAlphavalue = 1.0;

Then add the following to drawWheel: right before section #8:

	// 7.1 - Add background image
	UIImageView *bg = [[UIImageView alloc] initWithFrame:self.frame];
	bg.image = [UIImage imageNamed:@"bg.png"];
	[self addSubview:bg];	

Also add the center button, which you can use to fire an action once a sector has been selected, right after the above:

UIImageView *mask = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 58, 58)];
mask.image =[UIImage imageNamed:@"centerButton.png"] ;
mask.center = self.center;
mask.center = CGPointMake(mask.center.x, mask.center.y+3);
[self addSubview:mask];

Replace sections #3, #4, #5, and #6 to get rid of the labels and use image views instead to show the segment.png, and add an icon to each segment:

	// 3 - Create the sectors
	for (int i = 0; i < numberOfSections; i++) {
        // 4 - Create image view
        UIImageView *im = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"segment.png"]];
        im.layer.anchorPoint = CGPointMake(1.0f, 0.5f);
        im.layer.position = CGPointMake(container.bounds.size.width/2.0-container.frame.origin.x, 
                                        container.bounds.size.height/2.0-container.frame.origin.y); 
        im.transform = CGAffineTransformMakeRotation(angleSize*i);
        im.alpha = minAlphavalue;
        im.tag = i;
        if (i == 0) {
            im.alpha = maxAlphavalue;
        }
		// 5 - Set sector image        
        UIImageView *sectorImage = [[UIImageView alloc] initWithFrame:CGRectMake(12, 15, 40, 40)];
        sectorImage.image = [UIImage imageNamed:[NSString stringWithFormat:@"icon%i.png", i]];
        [im addSubview:sectorImage];
        // 6 - Add image view to container
        [container addSubview:im];
	}

To identify the correct sector each time it’s needed, add a new helper method definition at the top of SMRotaryWheel.m:

@interface SMRotaryWheel()
	...
    - (UIImageView *) getSectorByValue:(int)value;
@end

Then add the method implementation at the end of SMRotaryWheel.m:

- (UIImageView *) getSectorByValue:(int)value {
    UIImageView *res;
    NSArray *views = [container subviews];
    for (UIImageView *im in views) {
        if (im.tag == value)
            res = im;
    }
    return res;
}

Whenever the user taps on the wheel, you have to reset the alpha value of the current sector to its minimum. Add the following to beginTrackingWithTouch:withEvent before the return YES.

	// 5 - Set current sector's alpha value to the minimum value
	UIImageView *im = [self getSectorByValue:currentSector];
	im.alpha = minAlphavalue;

Once a new sector is selected it has to be highlighted, so at the end of endTrackingWithTouch:withEvent:, add the following:

	// 10 - Highlight selected sector
	UIImageView *im = [self getSectorByValue:currentSector];
	im.alpha = maxAlphavalue;	

If you’d like to show the icon name instead of the position number, here’s a helper method to return the corresponding string.

- (NSString *) getSectorName:(int)position {
    NSString *res = @"";
    switch (position) {
        case 0:
            res = @"Circles";
            break;
            
        case 1:
            res = @"Flower";
            break;
            
        case 2:
            res = @"Monster";
            break;
            
        case 3:
            res = @"Person";
            break;
            
        case 4:
            res = @"Smile";
            break;
            
        case 5:
            res = @"Sun";
            break;
            
        case 6:
            res = @"Swirl";
            break;
            
        case 7:
            res = @"3 circles";
            break;
            
        case 8:
            res = @"Triangle";
            break;
            
        default:
            break;
    }
    return res;
}

If you want to use the above method, substitute all the calls to wheelDidChangeValue: in SMRotaryWheel.m with the following:

	[self.delegate wheelDidChangeValue:[self getSectorName:currentSector]];

You're almost done. Remember how we set the number of sectors in SMViewController.m to 12 in a previous code change? Well, the icon set only contains 8 icons. So we need to change the number of sectors back to 8 in SMViewController.m.

You're done! Now when the user is in the process of dragging the wheel, all the sectors are slightly faded. When one is selected it goes fully visible, while the others are still faded. You can tweak this effect as you wish – for example, by adding a gradient or a border.

Contributors

Over 300 content creators. Join our team.