# Customizing your carousel path

Posted on 15 Jul 2013 Written by Alison ClarkeOne of the fantastic new features of ShinobiEssentials 2.0 is the carousel. Out of the box, you can easily create linear carousels, cylindrical carousels, cover flows and more…but what if you want something a little bit different? Well that’s possible too of course – with just a little work.

This tutorial takes you through the process of creating a custom carousel. The example used mimics a conveyor belt, and attempts to reproduce the effect of the iOS7 Safari tabs screen. If you get stuck along the way, you can see the finished project on GitHub, or download it as a zip.

To get started, you’ll need a copy of Xcode and also (of course) a copy of ShinobiEssentials (you can download a trial here). If you haven’t used the ShinobiEssentials carousel before, it might be a good idea to read through the Quick Start Guide before continuing.

### Setting things up

First, create a new Single View Application in Xcode. We’ll turn on ARC to make things simpler, but we don’t need Storyboards or Unit testing, and we’ll set the application up for iPad.

Your project will open on the summary page. Go to the “Linked Frameworks and Libraries” section and add **QuartzCore.framework** and **Security.framework**. You also need to drag-drop in **ShinobiEssentials.embeddedFramework**, to use both the framework and bundled Resources. To check you did this correctly, open up the project window, go to the Build Phases tab and check that **ShinobiEssentials.framework** is under “Link Binary With Libraries”, and that under “Copy Bundle Resources”, there are lots of files beginning with “essentials_*.png” (such as “essentials_donebutton.png”).

If you’re using the trial version you’ll need to add your license key. Open up **AppDelegate.m**, import <ShinobiEssentials/SEssentials.h>, and set the license key inside `application:didFinishLaunchingWithOptions:`

as follows:

#import <ShinobiEssentials/SEssentials.h> @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [SEssentials setLicenseKey:@"your license key"]; … }

### Creating a carousel

Before we get into the nitty-gritty of creating a custom carousel path, let’s set up our carousel using one of the built-in types. In our demo app the items in the carousel will be `UIView`

s with a colored background, a white border, and a gradient shadow. We’ll store them in an array inside the `ViewController`

, which will be the carousel’s data source.

The first change to make is to open up **ViewController.xib** in Interface Builder, and change the background color of its view to black.

Next, open up **ViewController.h**, import ShinobiEssentials and declare that the class will implement the `SEssentialsCarouselDataSource`

protocol:

#import <UIKit/UIKit.h> #import <ShinobiEssentials/ShinobiEssentials.h> @interface ViewController : UIViewController<SEssentialsCarouselDataSource> @end

Now open up **ViewController.m** and add a couple of private variables at the top, to store the number of items in the carousel and the carousel views:

@implementation ViewController { int _numberOfItemsInCarousel; NSMutableArray *_carouselData; }

And then add a method to create our views:

- (void)setupCarouselData { // Create an array of coloured, bordered views to go in the carousel _carouselData = [[NSMutableArray alloc] init]; for (int i=0; i<=_numberOfItemsInCarousel; i++) { // Create a view UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width * 0.8, 300)]; // Add a subview, colored depending on its position in the carousel, with a border UIView *borderedView = [[UIView alloc] initWithFrame:view.bounds]; borderedView.backgroundColor = [UIColor colorWithHue:((float)i)/_numberOfItemsInCarousel saturation:1.0 brightness:0.5 alpha:1.0]; borderedView.layer.borderWidth = 2.f; borderedView.layer.borderColor = [UIColor whiteColor].CGColor; borderedView.layer.shouldRasterize = YES; [view addSubview:borderedView]; // Apply a shadow to the outer view, so it applies to the border as well as the contents CAGradientLayer *shadowGradient = [CAGradientLayer layer]; shadowGradient.startPoint = CGPointMake(0.5, 0); shadowGradient.endPoint = CGPointMake(0.5, 1); shadowGradient.locations = @[@0, @1]; shadowGradient.colors = @[(id)[UIColor colorWithWhite:0 alpha:0].CGColor, (id)[UIColor colorWithWhite:0 alpha:0.9f].CGColor]; shadowGradient.frame = view.bounds; [view.layer addSublayer:shadowGradient]; // Add the view to the carousel data [_carouselData addObject:view]; } }

Hopefully the comments make this method reasonably clear: colors, gradients and borders aren’t the main point of this tutorial so I’m not going to go into any detail about them here.

Our carousel needs an `SEssentialsDataSource`

to provide its data, so we need to implement the `SEssentialsCarouselDataSource`

protocol. There are two methods needed: `numberOfItemsInCarousel:`

(hopefully self-explanatory) and `carousel:itemAtIndex:`

which returns the `UIView`

to be displayed at the given index of the carousel. As we’ve already got an array of `UIView`

s, our implementation of these methods in **ViewController.m** is trivial:

#pragma mark - SEssentialsCarouselDataSource methods -(int)numberOfItemsInCarousel:(SEssentialsCarousel *)carousel { return _numberOfItemsInCarousel; } -(UIView *)carousel:(SEssentialsCarousel *)carousel itemAtIndex:(int)index { return _carouselData[index]; }

Now, to get a carousel up and running, inside `viewDidLoad`

we need to define how many items we want, call our `setupCarouselData`

method, create and set up a carousel (we’ll use a vertical linear 2D one for now), and add it to our view:

- (void)viewDidLoad { [super viewDidLoad]; // Set up the carousel data _numberOfItemsInCarousel = 10; [self setupCarouselData]; // Create a carousel SEssentialsCarouselLinear2D *carousel = [[SEssentialsCarouselLinear2D alloc] initWithFrame:self.view.bounds]; carousel.orientation = SEssentialsCarouselOrientationVertical; carousel.dataSource = self; [self.view addSubview:carousel]; }

If you run the app now, you’ll see a carousel which lets you scroll through the views from bottom to top:

### Customizing your carousel path

To create a custom carousel path, we need to subclass `SEssentialsCarousel`

. There are two methods you can override to set up your custom path: `positionOfItemAtOffset:`

, which allows you to move the carousel items in two dimensions, and `transformOfItemAtOffset:`

, which allows you to do more complex transformations. There’s an example using `positionOfItemAtOffset:`

in the how-to guides in the ShinobiEssentials API docs, but today we’re going to concentrate on using `transformOfItemAtOffset:`

. (It is possible to combine the two, in which case `positionOfItemAtOffset:`

is called first, but for our purposes the code is simpler if we do everything in one method.)

To start creating your custom carousel, create a new Objective-C class called **ConveyorBeltCarousel**, a subclass of `SEssentialsCarousel`

. Add some properties to **ConveyorBeltCarousel.h**:

/** The maximum number of items to fit on the front of the conveyor belt (defaults to 5)*/ @property (nonatomic, assign) int maxNumberOfItemsOnFront; /** The radius of the rollers on the end of the conveyor belt (defaults to 150)*/ @property (nonatomic, assign) float rollerRadius; /** The rotation applied to the off-center items (defaults to 0.1) */ @property (nonatomic, assign) float rotateFactor;

And initialize them in `initWithFrame:`

in **ConveyorBeltCarousel.m**:

- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // Set up some default values self.panVector = CGPointMake(0, 1); self.maxNumberOfItemsOnFront = 5; self.rollerRadius = 150.f; self.rotateFactor = 0.1f; } return self; }

The `panVector`

line in the initializer makes the carousel scroll in response to vertical swipes.

We’ll start with a fairly straightforward implementation of `transformOfItemAtOffset:`

which just spaces the items out vertically (like the 2D linear carousel we just saw) and rotates the items away from us. The `offsetFromFocusPoint`

provided to this method is the distance in indices between the item and the focus point of the carousel. So in the initial position of the carousel, the 0^{th} item will have offset 0, the 1^{st} item will have offset 1, etc; as the items scroll up, these offsets will increase. In our initial implementation we just work out how far apart the items should be (based on the height of the carousel and `maxNumberOfItemsOnFront`

), then scale up the offset by this spacing. We then create a `CATransform3D`

and use it to translate the item up the y-axis by the calculated offset.

The next effect we apply is to modify the `CATransform3D.m24`

and `CATransform3D.m34`

properties. This has the effect of changing the camera angle so we get a better view of the rotation.

The final transformation we apply (for now) is a rotation away from us around the x-axis, based on rotateFactor.

So our initial attempt at a `transformOfItemAtOffset:`

method looks like this:

- (struct CATransform3D)transformOfItemAtOffset:(float)offsetFromFocusPoint { // Work out how far apart the items should be float itemSpacing = self.bounds.size.height/self.maxNumberOfItemsOnFront; // Work out the yOffset float yOffset = itemSpacing * offsetFromFocusPoint; // Create the transform and translation CATransform3D transform = CATransform3DIdentity; transform = CATransform3DTranslate(transform, 0, yOffset, 0); // Next we change the "camera angle" and rotate the items downwards transform.m24 = 1/2000.f; transform.m34 = -1/500.f; transform = CATransform3DRotate(transform, -M_PI_2 * self.rotateFactor, 1, 0, 0); return transform; }

If you run the app now, you should get something like this:

It’s a bit like the 2D linear carousel, but the items are now rotated. That’s pretty cool (especially once you’ve got something interesting on your views), but it’s not much like a conveyor belt (or those iOS7 Safari tabs). So let’s do a bit more work on our transformation…

### Making a conveyor belt

Before we get into the details of the transformations for the conveyor belt, let’s take a look at what we’re attempting to do. Here’s a view of the conveyor belt from the side, with some distances marked which will help us in our calculations:

`rollerRadius`

is already a property of the `ConveyorBeltCarousel`

, but we need to work out `itemSpacing`

and `rollerBoundary`

. We’ll store these as private variables in **ConveyorBeltCarousel.m**:

@implementation ConveyorBeltCarousel { // _rollerBoundary is the distance from the middle of the conveyor belt to the middle of the roller, // and is calculated so that the conveyor belt fits exactly in the view float _rollerBoundary; // _itemSpacing is the distance between the items on the carousel float _itemSpacing; }

We’ll fit the conveyor belt exactly in our frame, so we need `_rollerBoundary`

plus `rollerRadius`

to be half of the frame height. Then, we calculate `_itemSpacing`

to fit `maxNumberOfItemsOnFront`

items on the “front” of the conveyor belt (the flat bit between the rollers), i.e. on `2 * _rollerBoundary`

. We’ll add a method to **ConveyorBeltCarousel.m** to calculate these values:

- (void)calculateBoundaryAndSpacing { // Calculate the roller boundary based on the view height and rollerRadius _rollerBoundary = self.bounds.size.height/2 - self.rollerRadius; // Calculate the item spacing so that maxNumberOfItemsOnFront will fit onto the "flat" part // of the conveyor belt _itemSpacing = _rollerBoundary * 2/self.maxNumberOfItemsOnFront; }

We’ll call the method in `initWithFrame:`

, after setting up the properties. We’ll also add observers so we can recalculate the `_rollerBoundary`

and `_itemSpacing`

whenever the properties `rollerRadius`

or `maxNumberOfItemsOnFront`

change:

// Calculate the roller boundary and the item spacing [self calculateBoundaryAndSpacing]; // Add observers so we can update the boundary and spacing whenever the rollerRadius or // maxNumberOfItemsOnFront are changed [self addObserver:self forKeyPath:@"rollerRadius" options:NSKeyValueObservingOptionNew context:nil]; [self addObserver:self forKeyPath:@"maxNumberOfItemsOnFront" options:NSKeyValueObservingOptionNew context:nil];

We then add an implementation of `observeValueForKeyPath:ofObject:change:context:`

to respond to the observers, and make sure we remove the observers in dealloc:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqual:@"rollerRadius"] || [keyPath isEqual:@"maxNumberOfItemsOnFront"]) { // If our rollerRadius or maxNumberOfItemsOnFront properties have changed, we need to // recalculate the rollerBoundary and itemSpacing, and redraw the carousel. [self calculateBoundaryAndSpacing]; [self redrawCarousel]; } else { // Otherwise we pass the observations up to the parent [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } -(void)dealloc { // Remove the observers [self removeObserver:self forKeyPath:@"rollerRadius"]; [self removeObserver:self forKeyPath:@"maxNumberOfItemsOnFront"]; }

So we’ve got our variables set up; now we need to modify our `transformOfItemAtOffset:`

method to provide a more appropriate transformation. We’ll work out a scaled offset based on `_itemSpacing`

as we did before: this is the distance around the conveyor belt, from the middle of the front. We’ll use the absolute value for now to make the math easier:

float scaledOffset = _itemSpacing * fabsf(offsetFromFocusPoint);

(I’ll show you the finished function shortly, so don’t worry about copying all of these code snippets.)

From this basic value we need to calculate appropriate offsets on the y and z axes. We’ve got three cases to consider:

- scaledOffset is smaller than rollerBoundary.
- scaledOffset is bigger than rollerBoundary by an amount less than half the circumference of the roller.
- scaledOffset is bigger than rollerBoundary plus half the circumference of the roller.

**Case 1** is the simplest case, where the item is on the front of the conveyor belt, and the yOffset is simply `scaledOffset`

(made negative again where necessary), and the zOffset is 0.

In **case 2**, the item is on the end of the roller, so we need to do a bit of trigonometry to calculate our y and z offsets. I’m going to go back to basics here – read on to case 3 if these calculations are obvious to you! Here’s the top of the conveyor belt again, with some new properties marked:

Here, *r* is the `rollerRadius`

, and *s* is the difference between `scaledOffset`

and `_rollerBoundary`

, and from these we need to calculate *θ* (our angle), *o* (opposite) and *a* (adjacent), to work out our y and z offsets.

We’ll do our workings in radians (2*π* radians = 360°), so *θ* is simply *s* divided by the radius:

float angle = (scaledOffset - _rollerBoundary)/self.rollerRadius;

Next, you might remember from school that *o* = *r* sin*θ*, and our total y offset is `_rollerBoundary`

plus *o*, calculated as follows:

yOffset = _rollerBoundary + self.rollerRadius * sin(angle);

Similarly, *a* = *r* cos*θ*, and our z offset is the difference between rollerRadius and *a*: as it’s going away from us we need a negative z offset, so we calculate it as follows:

zOffset = self.rollerRadius * (cos(angle) - 1);

(Because of the shape of the sine and cosine functions, these calculations give us the right answers for obtuse angles as well as acute ones, but I won’t go into the details of that here.)

**Case 3** is where the item is on the back of the conveyor belt. The `scaledOffset`

goes round the roller and back down the other side, so the y offset is half the length of the conveyor belt (i.e. `2*_rollerBoundary + π*rollerRadius`

) minus `scaledOffset`

:

yOffset = 2 * _rollerBoundary + M_PI * self.rollerRadius - scaledOffset;

And the z offset is twice the roller radius, negative because it’s away from us:

zOffset = -2 * self.rollerRadius;

(We’re not going to cover the case where `_itemSpacing`

is more than half the length of the carousel, as there’s not really anything sensible we can do with the transformation. The calculations in case 3 will apply in these cases, so the items will continue to travel up or down at the back of the carousel. If you’ve got more items than will fit on the conveyor belt, you can adjust the carousel’s `maxNumberOfItemsToDisplay`

property, to hide the items with too great an offset.)

The final bits of work we need to do are to make the y offset negative if the original offset was negative, and to apply the y and z translations, as well as the rotation we used previously. We need to apply the y translation first, then the rotation, and finally the z translation, to achieve the effect we want. The final function looks like this:

- (struct CATransform3D)transformOfItemAtOffset:(float)offsetFromFocusPoint { CATransform3D transform = CATransform3DIdentity; // Work out how far from the front middle of the conveyor belt the item should be float scaledOffset = _itemSpacing * fabsf(offsetFromFocusPoint); // The y offset is based on the scaledOffset, but if that's near or past the end of the conveyor // belt, it needs to be adjusted so it looks like it's rolling round the end float yOffset, zOffset; if (scaledOffset < _rollerBoundary) { // The item is on the "front" of the conveyor belt, so the yOffset is just scaledOffset, // and the zOffset is 0 yOffset = scaledOffset; zOffset = 0; } else if (scaledOffset < _rollerBoundary + M_PI * self.rollerRadius) { // The item is on the roller of the conveyor belt, because its offset is less than the // rollerBoundary plus half the circumference of the roller. // Work out the angle from the middle of the roller to the item: because we're working // in radians, it's just the distance around the circumference divided by the radius float angle = (scaledOffset - _rollerBoundary)/self.rollerRadius; // The y distance above the roller boundary is r * sin(angle) yOffset = _rollerBoundary + self.rollerRadius * sin(angle); // The z distance away from us is r - r * cos(angle); we need the negative of that to move // the item away from rather than towards us zOffset = self.rollerRadius * (cos(angle) - 1); } else { // The item is on the "back" of the conveyor belt because its offset is greater // than the rollerBoundary plus half the circumference of the roller. // The yOffset we need is the half the length of the conveyor belt, minus scaledOffset. // The length of the belt is 4 * rollerBoundary + 2 * pi * rollerRadius yOffset = 2 * _rollerBoundary + M_PI * self.rollerRadius - scaledOffset; // The zOffset is just the diameter of the rollers, negative because it's away from us zOffset = -2 * self.rollerRadius; } // Sort out negative offsets so they're still negative if (offsetFromFocusPoint < 0) { yOffset = -yOffset; } // We apply the y translation first transform = CATransform3DTranslate(transform, 0, yOffset, 0); // Next we change the "camera angle" and rotate the items downwards transform.m24 = 1/2000.f; transform.m34 = -1/500.f; transform = CATransform3DRotate(transform, -M_PI_2 * self.rotateFactor, 1, 0, 0); // Finally we apply the z translation transform = CATransform3DTranslate(transform, 0, 0, zOffset); return transform; }

If you run the carousel now, you’ll start to see the conveyor belt effect, but it’s not quite right yet. It would work OK for smaller items, but the “camera angle” means that we can’t see the top of the carousel, and because we’re concentrating on the position of the top of the items, there’s a slightly odd effect at the bottom end of the carousel. We could probably do some more complicated things with the transformation to make this look better, but as the effect we’re really after is something like those iOS7 Safari tabs, we don’t really need to worry about what happens at the bottom: instead we’ll move the carousel so we can just see the top of it. We’ll also tweak its momentum, and add a nice panning effect when it first loads. We’ll do all this back in `viewDidLoad`

in **ViewController.m**:

- (void)viewDidLoad { [super viewDidLoad]; // Set up the carousel data _numberOfItemsInCarousel = 10; [self setupCarouselData]; // Create a conveyor belt carousel ConveyorBeltCarousel *carousel = [[ConveyorBeltCarousel alloc] initWithFrame:self.view.bounds]; carousel.dataSource = self; // Adjust the focus point so we don't see the bottom part of the conveyor belt carousel.focusPointNormalized = CGPointMake(0.5, 0.7); // Make the momentum last a bit longer carousel.frictionCoefficient = 0.8; // Add the view [self.view addSubview:carousel]; // Pan up to the 4th item [carousel panToItemAtIndex:3 animated:YES withDuration:0.5]; }

If you run the app now, you’ll see a lovely effect as the items roll over the top of the conveyor belt. Maybe it’s not quite as smooth as those Safari tabs, but it’s a good start! If you’d like to improve on it (or if you got a bit stuck following the tutorial), the finished project is on GitHub. We’d love to see what you can do with custom carousel paths – please get in touch to tell us about it!

Back to Blog