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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Create a Rotating Wheel Control with UIKit
50 mins
Laying out the Wheel
Now that we’ve got a basic control class set up, we can start to visually arrange the elements of the wheel so that they appear in a circle.
To position the elements of the wheel, you will leverage the power of CALayers. This power not only enables the creation of cool effects, it also allows you to manipulate geometrical properties like rotation. If you aren’t acquainted with CALayers, check out our Introduction to CALayers tutorial.
To use layers, you need to import the QuartzCore framework. To do this, select the project in the left sidebar and then select Target and then the Build Phases tab on the right pane. Expand the “Link Binary with Libraries” section and then click on the + sign and look for QuartzCore to add it.
After you complete this step, the framework should appear somewhere in your project tree similar to the following image:
Now for the initial implementation of the layout. Initially, each sector will be an instance of UILabel. Later on you’ll switch to a UIImageView with a custom image.
For now, you’ll use a simple red background so the borders of the label are visible, and you’ll fill each label with a number. The effect you want to achieve is similar to the unfolding of a deck of cards into a circle. In this case, you want the cards perfectly distributed along a full circle, so you need a way to apply a rotation to each label.
Here’s the intended result:
The rotation of a CALayer is applied according to the anchorPoint property, which is an instance of CGPoint ranging from (0,0) to (1,1). The anchorPoint is the pivot point for the layer. The following image identifies some values for the anchorPoint:
By default, the anchorPoint is set to (0.5,0.5) – that is the center of the layer’s frame, but you can change this to whatever you want.
In this tutorial, we want to rotate a each label around the center of the circle. To do this, we just follow these steps for each label:
- Set the anchor point to the right center (1.0, 0.5).
- Position the label so that the right center matches up to the center of the circle.
- Apply a transform to rotate the label the appropriate amount!
So let’s get going with drawing our wheel! But first, you must add an import for QuartzCore to the top of SMRotaryWheel.m:
#import <QuartzCore/QuartzCore.h>
Then, replace the empty drawWheel method from our initial implementation with the following code:
- (void) drawWheel {
// 1
container = [[UIView alloc] initWithFrame:self.frame];
// 2
CGFloat angleSize = 2*M_PI/numberOfSections;
// 3
for (int i = 0; i < numberOfSections; i++) {
// 4
UILabel *im = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 40)];
im.backgroundColor = [UIColor redColor];
im.text = [NSString stringWithFormat:@"%i", i];
im.layer.anchorPoint = CGPointMake(1.0f, 0.5f);
// 5
im.layer.position = CGPointMake(container.bounds.size.width/2.0,
container.bounds.size.height/2.0);
im.transform = CGAffineTransformMakeRotation(angleSize * i);
im.tag = i;
// 6
[container addSubview:im];
}
// 7
container.userInteractionEnabled = NO;
[self addSubview:container];
}
Let's go over how this works bit by bit.
- Here we create a view that we'll put everything else inside.
- There are 2 * PI radians in a circle (more on this later), so we divide the number of radians by the number of sections we wish to display in our control. This gives us an amount we have to rotate between sections, which we'll use later.
- For each section, we create a label and set the anchor point to the middle right, as explained earlier.
- We set the anchor point to the middle right, so now when we set the position it moves the middle right of the label to that point. So here we set the position of the label (the middle right) to the center of the container view. To rotate the label, we can simply set the transform of the label to a rotation transform. Don't worry, you don't need to know math to do this - you can use the built in CGAffineTransformMakeRotation method! We just multiply the amount to rotate per section by the number of sections so far.
- Adds the label to the container view we created earlier.
- Adds the container to the main control.
To make sure the application will display something, we need to add an instance of the wheel class to SMViewController. Add the following import statement to to the top of SMViewController.m:
#import "SMRotaryWheel.h"
Next, override viewDidLoad as follows by replacing the existing method code:
- (void)viewDidLoad {
// 1 - Call super method
[super viewDidLoad];
// 2 - Set up rotary wheel
SMRotaryWheel *wheel = [[SMRotaryWheel alloc] initWithFrame:CGRectMake(0, 0, 200, 200)
andDelegate:self
withSections:8];
// 3 - Add wheel to view
[self.view addSubview:wheel];
}
If you run the application now, you will see the red rosette as per the image above. You know you’re on the right track!
Now it’s time to dig into some theory about circles, angles, pi, degrees and radians. Armed with this understanding, you’ll be able to correctly rotate the container of sectors according to the movements made by the user when touching (and dragging) the component.
A Digression Into Trigonometry
We all learned in school how to measure angles in degrees, and we all know that there are 360 degrees in a circle. But scientists, engineers and programming language creators use a unit called radians.
You might remember that the code above for drawWheel uses the expression 2*M_PI in section #2 to calculate the size of the circle and split it into sectors. That’s because 360 degrees correspond exactly to 2*M_PI. Using this formula, we can deduce that 1 radian equals 180/pi and that 1 degree equals pi/180 radians.
That gives us the formula for conversion between degrees and radians! But let’s show the relationship visually.
The image above shows the "length" of one radian, which corresponds roughly to an angle of 57.29 degrees. We say approximately because pi is an infinite decimal number.
Here’s another way to explain it. If you took the segment of the circle’s perimeter between the red lines in the picture above, and you straightened it into a line, that line would have the same length as the radius of the circle.
In other words, if the arc length cut by an angle (whose vertex is at the center of the circle) is equal to the radius, then the measure of that angle is 1 radian. Pretty cool, eh?
An important observation is that, regardless of the length of the radius, in a full circle there are always 2*pi radians. This will be very helpful when you will apply the rotation transform to your rotating wheel. You’ll be "cutting the pie" into eight equal pieces, so each piece will be approximately 0.78 radians, calculated as 2*pi/8.
You’ll begin populating the circle from the left, going clockwise, so your zero is on the left. The figure below shows the values of degrees and radians in your current scenario, made of eight sectors.
The black dots represent the midpoints of each sector in radians. As you saw above in the drawWheel method, to change the rotation of an element you create an affine transform (of type rotation) and set it as a property of the container. Something like this:
CGAffineTransform t = CGAffineTransformRotate(container.transform, newValueInRadians);
container.transform = t;
Unfortunately, the newValueInRadians is not the point you want to rotate to – it’s the number of radians to be added/subtracted from the current value. You can’t say "rotate to x radians.” You have to calculate the difference between the current value and x, and then add/subtract it as needed.
For example, you can set up a timer to periodically rotate the wheel. Let's do that by first adding the method definition to SMRotaryWheel.h below the initWithFrame definition:
-(void)rotate;
Then, add the following code immediately after section #3 (but before the closing curly brace after section #3) in initWithFrame in SMRotaryWheel.m :
// 4 - Timer for rotating wheel
[NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(rotate)
userInfo:nil
repeats:YES];
Finally, add the rotate method to the end of SMRotaryWheel.m (but before the closing @end):
- (void) rotate {
CGAffineTransform t = CGAffineTransformRotate(container.transform, -0.78);
container.transform = t;
}
We chose -0.78 here because it's a hard-coded value for the amount of radians necessary to rotate one segment given that we have 8 segments, as discussed above.
Build and run. You’ll see the wheel completing a rotation every two seconds. This is more or less what you’ll be doing in the final application, though you’ll have to hook it up with user touches. And that brings us to the tricky part.