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

Building the Sectors

To have the wheel come to rest centred on the current sector, you first need to split the wheel into sectors. We'll do the following when the user lifts their finger:

  1. Calculate the current value in radians
  2. Find out the sector based on the calculated value
  3. Animate the radians to the midpoint of that sector

For example, if the selected sector is zero and the user drags the wheel only slightly up or down, you want the rotation to animate back to the centre of sector zero. Since this could get a little tricky, let's go over the steps in detail.

But first, let’s get a better understanding of the world of the container.

Add the following at the top of continueTrackingWithTouch:withEvent in SMRotaryWheel.m:

CGFloat radians = atan2f(container.transform.b, container.transform.a);
NSLog(@"rad is %f", radians);

This logs the rotation of the container each time the user drags their finger. You’ll notice that if the wheel is dragged clockwise, there are positive values until the rotation is greater than pi radians (180 degrees) or, if you prefer, when the sector labeled “0” is in the quadrant below the center horizontal. When you go beyond 180 degrees, you’ll see negative values, as in the following screenshot.

This is what you have to account for when calculating the sector boundaries: their min, mid and max values. The selected sector will always be the one in the leftmost position – the initial position of the 0 sector. So in building the sectors, you’re addressing the following question: when the value of radians is x, which sector is identified by the placeholder?

To answer this question, you need to reason backwards. The following image shows a wheel with eight sectors.

The values around the circumference of the circle represent the min and max radian values for each sector. For example, whenever the value of the container's radians is between -0.39 and 0.39, the rotation should be set to the midpoint of sector 0.

Again, you have to take into account the quadrant (positive or negative) to correctly add/subtract the difference of the angle. In this particular case, you have to deal with the fact that both sectors 0 and 4 spans two quadrants. In the case of sector 0, the center point is 0 radians and so it's fairly straightforward. However, for sector 4, the centre could be either pi or -pi since the center point straddles the dividing line between the negative and positive quadrants. So things can get a little bit confusing here.

You can see in the following image that if there were an odd number of sectors, the value for the centre point would be a bit simpler.

In order to be flexible (and comprehensive), this tutorial will account for both an even and odd number of sectors, and provide separate procedures to build them. But first, we have to define a new class to represent each sector so that we can store min, mid and max values for each sector.

Create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class SMSector, and make it a subclass of NSObject. Now switch to SMSector.h and replace its contents with the following:

@interface SMSector : NSObject

@property float minValue;
@property float maxValue;
@property float midValue;
@property int sector;

@end

And then switch to SMSector.m and replace its contents with the following implementation code:

#import "SMSector.h"

@implementation SMSector

@synthesize minValue, maxValue, midValue, sector;

- (NSString *) description {
    return [NSString stringWithFormat:@"%i | %f, %f, %f", self.sector, self.minValue, self.midValue, self.maxValue];
}

@end

In SMRotaryWheel.h import the SMSector header:

#import "SMSector.h"

Then, add a new property, named sectors:

@property (nonatomic, strong) NSMutableArray *sectors;

Now switch to SMRotaryWheel.m and add two new helper method definitions to build the sectors (just below the existing definition for calculateDistanceFromCenter:):

@interface SMRotaryWheel()
    ...
    - (void) buildSectorsEven;
    - (void) buildSectorsOdd;
@end

Then, synthesize the new property:

@synthesize sectors;

Next, at the end of drawWheel, add the following code so that the sectors are initialized when you create a new wheel.

    // 8 - Initialize sectors
    sectors = [NSMutableArray arrayWithCapacity:numberOfSections];
    if (numberOfSections % 2 == 0) {
        [self buildSectorsEven];
    } else {
        [self buildSectorsOdd];
    }

Let's start with the simple case - when there are an odd number of sectors. Add the following method implementation to the bottom of SMRotaryWheel.m (before the @end):

- (void) buildSectorsOdd {
	// 1 - Define sector length
    CGFloat fanWidth = M_PI*2/numberOfSections;
	// 2 - Set initial midpoint
    CGFloat mid = 0;
	// 3 - Iterate through all sectors
    for (int i = 0; i < numberOfSections; i++) {
        SMSector *sector = [[SMSector alloc] init];
		// 4 - Set sector values
        sector.midValue = mid;
        sector.minValue = mid - (fanWidth/2);
        sector.maxValue = mid + (fanWidth/2);
        sector.sector = i;
        mid -= fanWidth;
        if (sector.minValue < - M_PI) {
            mid = -mid;
            mid -= fanWidth; 
        }
		// 5 - Add sector to array
        [sectors addObject:sector];
		NSLog(@"cl is %@", sector);
    }
}

Let's go through the above code step-by-step:

  1. First, we identify the length (or the width, if you prefer) of the sector in radians.
  2. Next, we initialize a variable with the initial midpoint. Since our starting point is always zero radians, that becomes our first midpoint.
  3. Then we iterate through each of the sectors to set up the min, mid, and max values for each sector
  4. When calculating the min and max values, you add/subtract half of the sector width to get the correct values. Remember that your range is from -pi to pi, so everything has to be "normalized" between those values. If a value is greater than pi or –pi, that means you’ve changed quadrant. Since you’ve populated the wheel clockwise, you have to take into account when the minimum value is less than pi, and in that case change the sign of the midpoint.
  5. Finally, once we have a sector set up, we add that sector to our previously defined sector array.

Now in the SMViewController.m, modify section #2 in viewDidLoad to set the number of sections to 3, as follows:

SMRotaryWheel *wheel = [[SMRotaryWheel alloc] initWithFrame:CGRectMake(0, 0, 200, 200)  
                                                    andDelegate:self 
                                                   withSections:3];

If you compile and run now, the console should show the following results.

These are exactly the same radian values that were in the 3-sectioned wheel in the image above. So your calculations worked as expected!

Now let's address the case of an even number of sectors by adding the following code to the end of SMRotaryWheel.m:

- (void) buildSectorsEven {
    // 1 - Define sector length
    CGFloat fanWidth = M_PI*2/numberOfSections;
    // 2 - Set initial midpoint
    CGFloat mid = 0;
    // 3 - Iterate through all sectors
    for (int i = 0; i < numberOfSections; i++) {
        SMSector *sector = [[SMSector alloc] init];
        // 4 - Set sector values
        sector.midValue = mid;
        sector.minValue = mid - (fanWidth/2);
        sector.maxValue = mid + (fanWidth/2);
        sector.sector = i;
        if (sector.maxValue-fanWidth < - M_PI) {
            mid = M_PI;
            sector.midValue = mid;
            sector.minValue = fabsf(sector.maxValue);
            
        }
        mid -= fanWidth;
        NSLog(@"cl is %@", sector);
        // 5 - Add sector to array
        [sectors addObject:sector];
    }
}

As you'll notice, the basic logic is the same as building an odd number of sectors. The main difference is that in this instance pi (or -pi if you move counterclockwise) is not a max or min point, but it coincides with a midpoint. So you have to check if, by subtracting the sector width from the max value, you pass the -pi limit, and if you do, set the min value as positive.

Now modify SMViewController.m's viewDidLoad method (section #2) to switch the code back to creating eight sectors and run the application. The sectors should all go from -pi to pi as follows.

Notice that sector 4 has a positive min value and a negative max value.

Contributors

Over 300 content creators. Join our team.