Crafting Custom Layouts Using UICollectionViewLayout - iOS UICollectionView: The Complete Guide, Second Edition (2014)

iOS UICollectionView: The Complete Guide, Second Edition (2014)

Chapter 5. Crafting Custom Layouts Using UICollectionViewLayout

In the preceding chapter, I wrote that UICollectionViewFlowLayout is great for line-based, breaking layouts and that you should always resort to using it first. Sometimes, however, our layouts are sufficiently complicated to warrant the use of something more powerful.UICollectionViewLayout is the superclass of UICollectionViewFlowLayout and it is hands on. You are responsible for everything—the layout of cells, the size of the collection view—everything. We’ll take a look at an example where you’d want to use it, revisit decoration views, and explore a little bit of changing between layouts programmatically with animation. At the end of this chapter, we build a really cool photos application using a web service and a few cool custom layouts.

Subclassing UICollectionViewLayout

I don’t want to scare you away from subclassing UICollectionViewLayout, but let me reiterate that this is a last resort to be used only when the option of subclassing UICollectionViewFlowLayout instead has been explored. Treat that as your warning, lest you find yourself writing a lot of code you don’t have to.

If your layout is not based on a line that breaks when it hits the edge of the screen, subclassing UICollectionViewLayout directly is probably for you. If you find yourself writing code to reproduce the logic in UICollectionViewFlowLayout, reconsider subclassing it directly.

UICollectionView does no heavy lifting for you; you have to do everything yourself. Let’s look at a relatively simple example to see what I mean.

When Apple introduced UICollectionView at WWDC 2012, they had a few sessions that talked about the class and its layouts. Unfortunately, the sample code they provided was sparse and riddled with inaccuracies or simplifications. We’re going to take a look at one of the layouts they produced—the circle layout—with our own twist.

Each one of our cells is going to be arranged in a circle around some point on the screen. (We’re going to “future proof” this for adding interactivity in the next chapter; I only discuss the layout aspects for now.) Each cell is the same distance from that point. We’ll also adjust thetransform3D of each cell so that it “points” to the center of the circle. Finally, we’ll revisit decoration views; it’s been a while since we dealt with them, and it’ll be fun to reapply some of our new techniques to them.

To make it fun, we’ll add two buttons in a navigation bar: one for adding new cells and one for deleting them (with animations, of course). We’ll also have a basic UICollectionViewFlow layout to show you how to animate in between layouts. Although this is supposed to be really easy, it can often require some ingenuity to get working correctly.

Start by creating an empty app. In the application delegate, create a UINavigationController property to be our window’s root view controller. Instantiate it with an instance of our own view controller. You should all be familiar with this process by now. Just don’t forget to add QuartzCore to the libraries you link against. I place #import <QuartzCore/QuartzCore.h> in my precompiled header so that I don’t have to import it in every file. See Listing 5.1 for the basic app setup.

Listing 5.1 Basic App Setup


- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.viewController = [[UINavigationController alloc]
initWithRootViewController:[[AFViewController alloc] init]];
self.viewController.navigationBar.barStyle = UIBarStyleBlack;
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}


Our model is going to be simple; it’s just an integer of the number of cells to display that we’ll increment and decrement as we add and remove cells. Really easy. We’ll create a property for this number and ones for our two layouts and our segmented control (see Listing 5.2).

Listing 5.2 Private Properties


@interface AFViewController ()

@property (nonatomic, assign) NSInteger cellCount;

@property (nonatomic, strong) AFCollectionViewCircleLayout *circleLayout;
@property (nonatomic, strong) AFCollectionViewFlowLayout *flowLayout;

@property (nonatomic, strong)
UISegmentedControl *layoutChangeSegmentedControl;

@end


Our loadView and viewDidLoad methods are also straightforward; they instantiate our properties and set up our navigation item, as shown in Listing 5.3.

Listing 5.3 Setting Up the View Controller


static NSString *CellIdentifier = @"CellIdentifier";

-(void)loadView
{
// Create our view

// Create instances of our layouts
self.circleLayout = [[AFCollectionViewCircleLayout alloc] init];
self.flowLayout = [[AFCollectionViewFlowLayout alloc] init];

// Create a new collection view with our flow layout and set
// ourself as delegate and data source.
UICollectionView *collectionView = [[UICollectionView alloc]
initWithFrame:CGRectZero
collectionViewLayout:self.circleLayout];
collectionView.dataSource = self;
collectionView.delegate = self;

// Register our classes so we can use our custom subclassed
// cell and header
[collectionView registerClass:[AFCollectionViewCell class]
forCellWithReuseIdentifier:CellIdentifier];

// Set up the collection view geometry to cover the whole screen
// in any orientation and other view properties.
collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;

// Finally, set our collectionView (since we are a collection
// view controller, this also sets self.view)
self.collectionView = collectionView;

// Setup our model
self.cellCount = 12;
}

- (void)viewDidLoad
{
[super viewDidLoad];

self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self
action:@selector(addItem)];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemTrash
target:self
action:@selector(deleteItem)];

self.layoutChangeSegmentedControl = [[UISegmentedControl alloc]
initWithItems:@[@"Circle", @"Flow"]];
self.layoutChangeSegmentedControl.selectedSegmentIndex = 0;
self.layoutChangeSegmentedControl.segmentedControlStyle =
UISegmentedControlStyleBar;
[self.layoutChangeSegmentedControl addTarget:self
action:@selector(layoutChangeSegmentedControlDidChangeValue:)
forControlEvents:UIControlEventValueChanged];
self.navigationItem.titleView = self.layoutChangeSegmentedControl;
}

-(void)layoutChangeSegmentedControlDidChangeValue:(id)sender
{
// This just swaps the two values
if (self.collectionView.collectionViewLayout == self.circleLayout)
{
[self.flowLayout invalidateLayout];
self.collectionView.collectionViewLayout = self.flowLayout;
}
else
{
[self.circleLayout invalidateLayout];
self.collectionView.collectionViewLayout = self.circleLayout;
}
}


The layoutChangeSegmentedControlDidChangeValue: implementation is very basic. We’ll add more to it later to spice things up with animations a bit. Notice that it invalidates layouts before giving them to the collection view to use. This is really important. If we don’t do this, the collection view might be in landscape orientation but be laid out with portrait calculations. I know this sounds like the kind of thing that should have been taken care of for you, but you have to do it yourself. We also have to explicitly enable rotations for iOS 6.

Listing 5.4 Enabling Rotations


-(BOOL)shouldAutorotate
{
return YES;
}

-(NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskAll;
}


The flow layout implementation is straightforward, as Listing 5.5 shows.

Listing 5.5 Simple Flow Layout


@implementation AFCollectionViewFlowLayout

-(id)init
{
if (!(self = [super init])) return nil;

self.itemSize = CGSizeMake(200, 200);
self.sectionInset = UIEdgeInsetsMake(13.0f, 13.0f, 13.0f, 13.0f);
self.minimumInteritemSpacing = 13.0f;
self.minimumLineSpacing = 13.0f;

self.insertedRowSet = [NSMutableSet set];
self.deletedRowSet = [NSMutableSet set];

return self;
}

@end


Now that we have our basic flow layout, let’s get the guts of this example: the circle layout (see Listing 5.6). Create a new class that extends UICollectionViewLayout. We’re going to override collectionViewContentSize to return simply the size of the collection view itself, preventing it from ever scrolling. We’ll also override prepareLayout to set up the center of our circle and its radius; we will grab the number of cells in the collection view here.

This could represent a conflict in the separation of concerns in our app’s architecture. After all, aren’t layouts supposed to be unaware of the data that they’re helping display? That is true. However, in this case, the number of cells being displayed affects the layout, so it’s appropriate to access this information.

Listing 5.6 Circle Layout


-(void)prepareLayout
{
[super prepareLayout];

CGSize size = self.collectionView.bounds.size;

self.cellCount = [[self collectionView] numberOfItemsInSection:0];
self.center = CGPointMake(size.width / 2.0, size.height / 2.0);
self.radius = MIN(size.width, size.height) / 2.5;
}

-(CGSize)collectionViewContentSize
{
CGRect bounds = [[self collectionView] bounds];
return bounds.size;
}

- (UICollectionViewLayoutAttributes
*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
UICollectionViewLayoutAttributes* attributes =
[UICollectionViewLayoutAttributes
layoutAttributesForCellWithIndexPath:path];

attributes.size = CGSizeMake(kItemDimension, kItemDimension);
attributes.center =
CGPointMake(self.center.x + self.radius * cosf(2 * path.item * M_PI /
self.cellCount - M_PI_2), self.center.y + self.radius *
sinf(2 * path.item * M_PI / self.cellCount - M_PI_2));

attributes.transform3D = CATransform3DMakeRotation(
(2 * M_PI * path.item / self.cellCount), 0, 0, 1);

return attributes;
}

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray* attributes = [NSMutableArray array];

for (NSInteger i = 0 ; i < self.cellCount; i++)
{
NSIndexPath* indexPath = [NSIndexPath
indexPathForItem:i inSection:0];
[attributes addObject:[self
layoutAttributesForItemAtIndexPath:indexPath]];
}

return attributes;
}


The layoutAttributesForItemAtIndexPath: might look a little confusing, but it’s just a simple formula for the points along a circle. We also rotate each cell to make its bottom edge parallel to a tangent of the circle.

Finally, we need to create our cell subclass and our UICollectionViewDataSource methods (see Listing 5.7).

Listing 5.7 Simple Cell Subclass


@interface AFCollectionViewCell ()

@property (nonatomic, strong) UILabel *label;

@end

@implementation AFCollectionViewCell

- (id)initWithFrame:(CGRect)frame
{
if (!(self = [super initWithFrame:frame])) return nil;

self.backgroundColor = [UIColor orangeColor];

self.label = [[UILabel alloc] initWithFrame:
CGRectMake(0, 0,
CGRectGetWidth(frame),
CGRectGetHeight(frame))];
self.label.backgroundColor = [UIColor clearColor];
self.label.textAlignment = NSTextAlignmentCenter;
self.label.textColor = [UIColor whiteColor];
self.label.font = [UIFont boldSystemFontOfSize:24];
[self.contentView addSubview:self.label];

return self;
}

-(void)prepareForReuse
{
[super prepareForReuse];

[self setLabelString:@""];
}

-(void)setLabelString:(NSString *)labelString
{
self.label.text = labelString;
}


-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
[super applyLayoutAttributes:layoutAttributes];

self.label.center = CGPointMake(
CGRectGetWidth(self.contentView.bounds) / 2.0f,
CGRectGetHeight(self.contentView.bounds) / 2.0f);
}

@end


The cell simply displays some text; it will be used by both of our layouts to display the cell item number. The implementation of applyLayoutAttributes: sets the center point of the label so that it will be interpolated during the layout change animation, later. We can’t useframe here because that will change the bounds of the label immediately instead of with animation. Listing 5.8 shows a basic collection view data source implementation.

Listing 5.8 Simple UICollectionViewDataSource Implementation


- (NSInteger)collectionView:(UICollectionView *)view
numberOfItemsInSection:(NSInteger)section;
{
return self.cellCount;
}

(UICollectionViewCell *)collectionView:
(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath;
{
AFCollectionViewCell *cell = (AFCollectionViewCell *)
[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier
forIndexPath:indexPath];

[cell setLabelString:[NSString
stringWithFormat:@"%d", indexPath.row]];

return cell;
}


Let’s run the app and see what it looks like. Figure 5.1 shows our app running.

Image

Figure 5.1 Basic circle layout

Not bad! Remember, this is a pretty simple layout. It doesn’t do anything fancy at all; it doesn’t even scroll. Let’s take a look at the flow layout. Remember, we didn’t do anything special in the flow layout ; it’s all baked in (see Figure 5.2).

Image

Figure 5.2 Basic flow layout

Let’s make this a little more interesting. Rotate the device and notice that the animation for the circle layout is not great. You actually have to switch layouts to get the collection view to realize that its orientation has changed. If you recall from the Cover Flow layout we did in Chapter 4, “Organizing Content with UICollectionViewFlowLayout,” we need to let the collection view layout know that it should invalidate itself when the bounds of the collection view change. Implement the following method in the circle layout; it will let the collection view know that the layout becomes invalid on any change to its bounds (such as the change on rotation) (see Listing 5.9).

Listing 5.9 Invalidating Layout on bounds Change


-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}


That’s better. But I know that we can do even better.

Animating UICollectionViewLayout Changes

Let’s change our implementation of layoutChangeSegmentedControlDidChange-Value: to explicitly animate the change in collection view layout (see Listing 5.10).

Listing 5.10 Changing Layouts with Animation


-(void)layoutChangeSegmentedControlDidChangeValue:(id)sender
{
// We need to explicitly tell the collection view layout
// that we want the change animated.
if (self.collectionView.collectionViewLayout == self.circleLayout)
{
[self.flowLayout invalidateLayout];
[self.collectionView
setCollectionViewLayout:self.flowLayout animated:YES];
}
else
{
[self.circleLayout invalidateLayout];
[self.collectionView
setCollectionViewLayout:self.circleLayout
animated:YES];
}
}


You must use the setCollectionViewLayout:animated: method to get animations; setting the collectionViewLayout property in an animation block is not enough. Although this method will still animate the change, some cells will be duplicated during the animation. It’s a shame that Apple can’t decide if collectionViewLayout is an implicitly animatable property or not.

Now that we animate the change in layouts, UICollectionView layout will interpolate the changes in the layout attributes for each cell.

Figure 5.3 shows two intermediate stages of the collection view layout change animation. As a developer, you get this animation for free from UICollectionView. Not bad!

Image

Figure 5.3 Layout change animation

Now that we have our layout change animations finished, let’s add some fancy insertion and deletion animations to match. We’ll do the flow layout first because it’s easier.

Remember that we need to implement prepareForCollectionViewUpdates: and finalizeCollectionViewUpdates so that we only animate the inserted or deleted items. We’ll create two mutable sets to hang onto the items that are being inserted or deleted.

We’ll add a fade animation to the cells and make them spin. I want them to spin clockwise, so our initial rotation before insertion will be -90° and our final rotation after deletion will be 90°. These will have to be specified in radians (see Listing 5.11).

Listing 5.11 Animating Insertions and Deletions


@interface AFCollectionViewFlowLayout ()

@property (nonatomic, strong) NSMutableSet *insertedRowSet;
@property (nonatomic, strong) NSMutableSet *deletedRowSet;

@end

@implementation AFCollectionViewFlowLayout

-(id)init
{
if (!(self = [super init])) return nil;

self.itemSize = CGSizeMake(200, 200);
self.sectionInset =
UIEdgeInsetsMake(13.0f, 13.0f, 13.0f, 13.0f);
self.minimumInteritemSpacing = 13.0f;
self.minimumLineSpacing = 13.0f;

// Must instantiate these in init or else they'll
// always be empty
self.insertedRowSet = [NSMutableSet set];
self.deletedRowSet = [NSMutableSet set];

return self;
}

-(void)prepareForCollectionViewUpdates:(NSArray *)updateItems
{
[super prepareForCollectionViewUpdates:updateItems];

[updateItems
enumerateObjectsUsingBlock:^(UICollectionViewUpdateItem *updateItem,
NSUInteger idx, BOOL *stop) {
if (updateItem.updateAction ==
UICollectionUpdateActionInsert)
{
[self.insertedRowSet
addObject:@(updateItem.indexPathAfterUpdate.item)];
}
else if (updateItem.updateAction ==
UICollectionUpdateActionDelete)
{
[self.deletedRowSet
addObject:@(updateItem.indexPathBeforeUpdate.item)];
}
}];
}

-(void)finalizeCollectionViewUpdates
{
[super finalizeCollectionViewUpdates];

[self.insertedRowSet removeAllObjects];
[self.deletedRowSet removeAllObjects];
}
- (UICollectionViewLayoutAttributes *)
initialLayoutAttributesForAppearingItemAtIndexPath:
(NSIndexPath *)itemIndexPath
{
if ([self.insertedRowSet containsObject:@(itemIndexPath.item)])
{
UICollectionViewLayoutAttributes *attributes = [self
layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
attributes.center = self.center;
return attributes;
}

return nil;
}

- (UICollectionViewLayoutAttributes
*)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath
*)itemIndexPath
{
if ([self.deletedRowSet containsObject:@(itemIndexPath.item)])
{
UICollectionViewLayoutAttributes *attributes =
[self layoutAttributesForItemAtIndexPath:
itemIndexPath];
attributes.alpha = 0.0;
attributes.center = self.center;
attributes.transform3D = CATransform3DConcat(CATransform3DMakeRotation((2
* M_PI * itemIndexPath.item / (self.cellCount + 1)), 0, 0, 1),
CATransform3DMakeScale(0.1, 0.1, 1.0));

return attributes;
}

return nil;
}

@end


Let’s add code to insert and delete items, as shown in Listing 5.12.

Listing 5.12 Inserting and Deleting Items


-(void)addItem
{
[self.collectionView performBatchUpdates:^{
self.cellCount = self.cellCount + 1;
[self.collectionView
insertItemsAtIndexPaths:@[[NSIndexPath
indexPathForItem:self.cellCount-1
inSection:0]]];
} completion:nil];
}

-(void)deleteItem
{
// Always have at least once cell in our collection view
if (self.cellCount == 1) return;

[self.collectionView performBatchUpdates:^{
self.cellCount = self.cellCount - 1;
[self.collectionView
deleteItemsAtIndexPaths:@[[NSIndexPath
indexPathForItem:self.cellCount
inSection:0]]];
} completion:nil];
}


That’s the complete implementation for animations for insertion and deletion. Notice that we didn’t have to do any custom animation work ourselves; we only had to override the existing methods and set existing properties.

The attributes have an alpha value of zero applied in both insertion and deletion animations. The transform3D property is used to rotate the cell by a quarter radians (90°) clockwise for each animation. In addition, we scale down the cell to 10% of its usual size. The order which we do these typically matters, but not in this case.

The order we concatenate transforms is usually important because CATrasform3D is not communicative. Concatenating transforms uses a post-order multiplication; so if you want a scale, followed by a translation, you need to concatenate the scale transform to the translation transform. Always apply the transforms in the opposite order you want them applied. See Figure 5.4 for our running app with insertion/deletion animations.

Image

Figure 5.4 Flow layout deletion animation

In the circle layout class, add the same insertedRowSet and deletedRowSet private properties and instantiate them in init. Also write identical implementations for prepareForCollectionViewUpdates: and finalizeCollectionViewUpdates, which I won’t include in Listing 5.13.

Listing 5.13 Animating Insertions and Deletions in the Circle Layout


(UICollectionViewLayoutAttributes *)
initialLayoutAttributesForAppearingItemAtIndexPath:
(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes *attributes = [super
initialLayoutAttributesForAppearingItemAtIndexPath:
itemIndexPath];

if ([self.insertedRowSet
containsObject:@(itemIndexPath.item)])
{
attributes = [self
layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
attributes.center = self.center;
return attributes;
}

return attributes;
}

(UICollectionViewLayoutAttributes *)
finalLayoutAttributesForDisappearingItemAtIndexPath:
(NSIndexPath *)itemIndexPath
{
// The documentation says that this returns nil. It is lying.
UICollectionViewLayoutAttributes *attributes = [super
finalLayoutAttributesForDisappearingItemAtIndexPath:
itemIndexPath];

if ([self.deletedRowSet containsObject:@(itemIndexPath.item)])
{
attributes = [self
layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
attributes.center = self.center;
attributes.transform3D =
CATransform3DConcat(
CATransform3DMakeScale(0.1, 0.1, 1.0),
CATransform3DMakeRotation(
(2 * M_PI * itemIndexPath.item /
(self.cellCount + 1)),
0, 0, 1));

return attributes;
}

return attributes;
}


You can see that we’re applying nearly the same animations for insertion and deletion.

One difference between the insertion and deletion animations is the rotation. For insertions, we don’t specify one beyond what we already calculate when calling layoutAttributesForItemAtIndexPath:. This won’t work for deletion, and the reason is very subtle. ThecellCount property is already updated by the time either initialLayoutAttributesForAppearingItemAtIndexPath: or finalLayout-AttributesForDisappearingItemAtIndexPath: are called. When inserting, this means that the rotation angle calculated in layoutAttributesForItemAtIndexPath: reflects the correct angle for the new number of cells, which is what we want. However, when deleting, we don’t want the cell to have the updated angle reflecting the new cellCount; we want it to have its old angle. This means that we need to recalculate the rotation angle.

We concatenate two 3D transforms: a scale down to 10% of the item’s size, and a rotation calculated with the old cellCount: cellCount + 1. Again, the order of the transforms is not important in this case. See Figure 5.5 for our new animation.

Image

Figure 5.5 Circle layout deletion animation

What we’ve got so far is pretty good; we’re animating all the things we can animate.

Let’s add a decoration view to the center of the circle layout that will point to the same location that a minute hand would point to, given the current time. This will remind us how to implement decorations views and show you that you use the same method you do withUICollectionViewFlowLayout.

First, let’s implement the decoration view class (see Listing 5.14). Recall that any decoration view must subclass UICollectionReusableView.

Listing 5.14 Implementing the Decoration View


@implementation AFDecorationView

- (id)initWithFrame:(CGRect)frame
{
if (!(self = [super initWithFrame:frame])) return nil;

self.backgroundColor = [UIColor whiteColor];

CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.colors = @[(id)[[UIColor blackColor] CGColor],
(id)[[UIColor clearColor] CGColor]];
gradientLayer.backgroundColor = [[UIColor clearColor]
CGColor];
gradientLayer.frame = self.bounds;

self.layer.mask = gradientLayer;

return self;
}

@end


We create a gradient mask that spans the length of the decoration view so that we can tell which side is which. It also looks pretty cool, but you don’t want to use CALayer’s mask property too heavily because it slows down view rendering.

Next, we need to register the decoration view class in our circle layout’s init method (see Listing 5.15).

Listing 5.15 Registering the Decoration View Class


-(id)init
{
if (!(self = [super init])) return nil;

self.insertedRowSet = [NSMutableSet set];
self.deletedRowSet = [NSMutableSet set];

[self registerClass:[AFDecorationView class]
forDecorationViewOfKind:AFCollectionViewFlowDecoration];

return self;
}


To display our decoration view, we need to add a decoration view UICollection-ViewLayoutAttributes object to our layoutAttributesForElementsInRect: implementation (see Listing 5.16).

Listing 5.16 Adding Decoration Views to the Collection View


-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray* attributes = [NSMutableArray array];

for (NSInteger i = 0 ; i < self.cellCount; i++)
{
NSIndexPath* indexPath = [NSIndexPath
indexPathForItem:i inSection:0];
[attributes addObject:[self
layoutAttributesForItemAtIndexPath:indexPath]];
}

if (CGRectContainsPoint(rect, self.center))
{
[attributes addObject:[self
layoutAttributesForDecorationViewOfKind:
AFCollectionViewFlowDecoration
atIndexPath:
[NSIndexPath indexPathForItem:0
inSection:0]]];
}

return attributes;
}


The check to make sure the rect contains the center point is probably superfluous, but good practice nonetheless. Now that we have added the decoration view to the collection view, we need to give it the appropriate transform, as shown in Listing 5.17.

Listing 5.17 Decoration View Layout Attributes


-(UICollectionViewLayoutAttributes *)
layoutAttributesForDecorationViewOfKind:
(NSString *)decorationViewKind
atIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *layoutAttributes =
[UICollectionViewLayoutAttributes
layoutAttributesForDecorationViewOfKind:
decorationViewKind withIndexPath:indexPath];

if ([decorationViewKind
isEqualToString:AFCollectionViewFlowDecoration])
{
CGFloat rotationAngle = 0.0f;

if ([self.collectionView.delegate
conformsToProtocol:
@protocol(AFCollectionViewDelegateCircleLayout)])
{
rotationAngle =
[(id<AFCollectionViewDelegateCircleLayout>)
self.collectionView.delegate
rotationAngleForSupplmentaryViewInCircleLayout:self];
}

layoutAttributes.size = CGSizeMake(20, 200);
layoutAttributes.center = self.center;
layoutAttributes.transform3D =
CATransform3DMakeRotation(rotationAngle, 0, 0, 1);

// Place the decoration view behind all the cells
layoutAttributes.zIndex = -1;
}

return layoutAttributes;
}


I’ve created an AFCollectionViewDelegateCircleLayout protocol that we use to query the collection view’s delegate to determine the rotation we should use (see Listing 5.18).

Listing 5.18 AFCollectionViewDelegateCircleLayout Implementation


-
(CGFloat)rotationAngleForSupplmentaryViewInCircleLayout:(AFCollectionViewCircleLa
yout *)circleLayout
{
CGFloat timeRatio = 0.0f;

NSDate *date = [NSDate date];
NSDateComponents *components = [[NSCalendar currentCalendar]
components:NSMinuteCalendarUnit fromDate:date];
timeRatio = (CGFloat)(components.minute) / 60.0f;

return (2 * M_PI * timeRatio);
}


It’s a simple implementation that grabs the current minute, makes the assumption that each hour has only 60 minutes (bad form, I know), and calculates the current angle of the minute hand of an analogue clock, shown in Figure 5.6.

Image

Figure 5.6 Circle layout decoration view

In iOS 6, decoration views were somewhat notorious for being unreliable, particularly in rotation animations. I spoke with some Apple engineers at WWDC 2013 and was able to bring a few edge cases to their attention.

Stacking Layouts

Let’s tie things together and make something like a real app. When I worked for 500px, I wrote their open source iOS SDK, which we’ll now use to make a basic app to display pictures from their website. We’ll also use the image downloader I wrote to download the images once we retrieve the URLs from the 500px API. The sample code for this project is called One Hundred Pixels, since this is about one-fifth of any real 500px app.

First, you need to register an application with 500px. This will get you a consumer key and consumer secret pair, which you need to sign API requests. Create a new application and include the 500px iOS SDK and the AFImageDownloader classes in the Xcode project. #import these into your precompiled header and set up the PXRequest class in the applicationDidFinishLaucningWithOptions: method (see Listing 5.19).

Listing 5.19 Setting Up Your Consumer Key


- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.viewController = [[UINavigationController alloc]
initWithRootViewController:
[[AFViewController alloc] init]];
self.viewController.navigationBar.barStyle = UIBarStyleBlack;
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];

[PXRequest setConsumerKey:@"YOUR_CONSUMER_KEY"
consumerSecret:@"doesn't matter for this app"];

return YES;
}


Now we’re ready to make API calls. We’re going to get the 20 most recent images in the Popular, Editors Choice, and Upcoming streams. Let’s treat each stream as its own section; create an enum to keep track of them. We’ll also create three private properties in our implementation file (see Listing 5.20).

Listing 5.20 Using enum to Differentiate Sections


enum {
AFViewControllerPopularSection = 0,
AFViewControllerEditorsSection,
AFViewControllerUpcomingSection,
AFViewControllerNumberSections
};

@interface AFViewController ()

@property (nonatomic, strong) AFCollectionViewFlowLayout *
flowLayout;
@property (nonatomic, strong) AFCollectionViewStackedLayout *
stackLayout;
@property (nonatomic, strong) AFCoverFlowFlowLayout *
coverFlowLayout;

@property (nonatomic, strong) NSMutableArray *popularPhotos;
@property (nonatomic, strong) NSMutableArray *editorsPhotos;
@property (nonatomic, strong) NSMutableArray *upcomingPhotos;

@end


I’ve also included three properties for layouts we haven’t defined yet. We’ll create those classes soon.

Let’s create our API requests to fetch the images, shown in Listing 5.21. First, we need to set up our mutable arrays and our collection view. Although we haven’t implemented the code for the following layouts, you can use a standard UICollectionViewFlowLayout for now to see the intermediate steps.

Listing 5.21 Setting Up Our View and Fetching from the API


-(void)loadView
{
// Create our view

// Create instances of our layouts
self.stackLayout = [[AFCollectionViewStackedLayout alloc]
init];
self.flowLayout = [[AFCollectionViewFlowLayout alloc] init];
self.coverFlowLayout = [[AFCoverFlowFlowLayout alloc] init];

// Create a new collection view with our flow layout and
// set ourself as delegate and data source.
UICollectionView *collectionView = [[UICollectionView alloc]
initWithFrame:CGRectZero
collectionViewLayout:self.stackLayout];
collectionView.dataSource = self;
collectionView.delegate = self;

// Register our classes so we can use our custom
// subclassed cell and header
[collectionView registerClass:[AFCollectionViewCell class]
forCellWithReuseIdentifier:CellIdentifier];
[collectionView registerClass:[AFCollectionViewHeaderView
class]
forCellWithReuseIdentifier:HeaderIdentifier];

// Set up the collection view geometry to cover the whole
// screen in any orientation and other view properties.
collectionView.autoresizingMask =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;

// Finally, set our collectionView (since we are a
// collection view controller, this also sets self.view)
self.collectionView = collectionView;

// Setup our model
self.popularPhotos = [NSMutableArray arrayWithCapacity:20];
self.editorsPhotos = [NSMutableArray arrayWithCapacity:20];
self.upcomingPhotos = [NSMutableArray arrayWithCapacity:20];
}

-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

void (^block)(NSDictionary *, NSError *) =
^(NSDictionary *results, NSError *error) {

NSMutableArray *array;
NSInteger section;

if ([[results valueForKey:@"feature"] isEqualToString:@"popular"])
{
array = self.popularPhotos;
section = AFViewControllerPopularSection;
}
else if ([[results valueForKey:@"feature"] isEqualToString:@"editors"])
{
array = self.editorsPhotos;
section = AFViewControllerEditorsSection;
}
else if ([[results valueForKey:@"feature"] isEqualToString:@"upcoming"])
{
array = self.upcomingPhotos;
section = AFViewControllerUpcomingSection;
}
else
{
NSLog(@"%@", [results valueForKey:@"feature"]);
}

NSInteger item = 0;
for (NSDictionary *photo in [results valueForKey:@"photos"])
{
NSString *url = [[[photo valueForKey:@"images"]
lastObject] valueForKey:@"url"];

[AFImageDownloader
imageDownloaderWithURLString:url
autoStart:YES
completion:^(UIImage *decompressedImage) {
[array addObject:decompressedImage];
[self.collectionView
reloadItemsAtIndexPaths:@[[NSIndexPath
indexPathForItem:item inSection:section]]];
}];

item++;
}
};

[PXRequest
requestForPhotoFeature:PXAPIHelperPhotoFeaturePopular
resultsPerPage:20
completion:block];
[PXRequest
requestForPhotoFeature:PXAPIHelperPhotoFeatureEditors
resultsPerPage:20
completion:block];
[PXRequest
requestForPhotoFeature:PXAPIHelperPhotoFeatureUpcoming
resultsPerPage:20
completion:block];
}


We’re reusing the same callback block for all three requests, saving on code duplication. Each request will return 20 photo objects, and we’ll use AFImageDownloader to fetch and decompress the JPEG images.

Our controller is going to always have 20 cells per section; we’re not going to add cells to our collection view as we download images because they might not be downloaded in the proper order, and it’s a lot more work than is in the scope of this chapter. For this reason, our data source methods are straightforward (see Listing 5.22).

Listing 5.22 Setting Up Our View and Fetching from the API


-(NSInteger)numberOfSectionsInCollectionView:
(UICollectionView *)collectionView
{
return AFViewControllerNumberSections;
}

(NSInteger)collectionView:(UICollectionView *)view
numberOfItemsInSection:(NSInteger)section;
{
return 20;
}

(UICollectionViewCell *)collectionView:
(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath;
{
AFCollectionViewCell *cell = (AFCollectionViewCell *)
[collectionView dequeueReusableCellWithReuseIdentifier:
CellIdentifier
forIndexPath:indexPath];

NSArray *array;

switch (indexPath.section) {
case AFViewControllerPopularSection:
array = self.popularPhotos;
break;
case AFViewControllerEditorsSection:
array = self.editorsPhotos;
break;
case AFViewControllerUpcomingSection:
array = self.upcomingPhotos;
break;
}

if (indexPath.row < array.count)
{
[cell setImage:array[indexPath.item]];
}

return cell;
}


The convenient thing about using an enum to record sections is that AFView-ControllerNumberSections will change automatically if we add or remove sections.

Now that we have our controller finished, let’s define our collection view cell. It’s similar to the image cells used in Chapter 4’s Cover Flow example. See Listing 5.23 for our cell subclass.

Listing 5.23 UICollectionViewCell Subclass


@interface AFCollectionViewCell ()

@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) UIView *maskView;

@end

@implementation AFCollectionViewCell

- (id)initWithFrame:(CGRect)frame
{
if (!(self = [super initWithFrame:frame])) return nil;

self.backgroundColor = [UIColor whiteColor];

self.imageView = [[UIImageView alloc]
initWithFrame:CGRectInset(CGRectMake(0, 0,
CGRectGetWidth(frame),
CGRectGetHeight(frame)), 10, 10)];

self.imageView.autoresizingMask =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
self.imageView.clipsToBounds = YES;
[self.contentView addSubview:self.imageView];

self.maskView = [[UIView alloc]
initWithFrame:CGRectMake(0, 0,
CGRectGetWidth(frame),
CGRectGetHeight(frame))];
self.maskView.backgroundColor = [UIColor blackColor];
self.maskView.autoresizingMask =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
self.maskView.alpha = 0.0f;
[self.contentView insertSubview:self.maskView
aboveSubview:self.imageView];

return self;
}

-(void)prepareForReuse
{
[super prepareForReuse];

[self setImage:nil];
}

-(void)applyLayoutAttributes:
(UICollectionViewLayoutAttributes *)layoutAttributes
{
[super applyLayoutAttributes:layoutAttributes];

self.layer.shouldRasterize = YES;
self.layer.shadowColor = [[UIColor blackColor] CGColor];
self.layer.shadowOffset = CGSizeMake(0, 3);
self.maskView.alpha = 0.0f;
if ([layoutAttributes
isKindOfClass:[AFCollectionViewLayoutAttributes class]])
{
self.layer.shadowOpacity =
[(AFCollectionViewLayoutAttributes *)layoutAttributes
shadowOpacity];
self.maskView.alpha =
[(AFCollectionViewLayoutAttributes *)layoutAttributes
maskingValue];
}
}

-(void)setImage:(UIImage *)image
{
[self.imageView setImage:image];
}


As you can see, we’re going to use a custom UICollectionViewLayoutAttributes class. Define it using two properties—shadowOpacity and maskingValue—both CGFloat values. Don’t forget to implement the NSCopying method, shown in Listing 5.24.

Listing 5.24 NSCopying Method for Custom Layout Attributes Class


@implementation AFCollectionViewLayoutAttributes

-(id)copyWithZone:(NSZone *)zone
{
AFCollectionViewLayoutAttributes *attributes = [super
copyWithZone:zone];

attributes.shadowOpacity = self.shadowOpacity;
attributes.maskingValue = self.maskingValue;

return attributes;
}

@end


We’re almost to the stage of running our app. We’re going to define three layout objects: The first will stack an entire section’s cells on top of one another, the second will display all the cells in a plain flow layout grid, and the third will use a layout similar to Cover Flow.

I again want to take the opportunity to thank Mark Pospesel for his work on IntroducingCollectionViews. The code for the stacked layout and the Cover Flow layout come almost wholesale from his work.

Some of the properties in the stacked layout only make sense in the context of a more interactive collection view, so we’ll be saving discussion of those aspects of the layout for the next chapter. If you’re curious, you can poke around the AFCollectionView-StackedLayoutimplementation.

An important thing I’ll mention now is the hidden property on UICollectionViewLayoutAttributes. This is an optimization that UICollectionView performs to stop itself from rendering cells which don’t need to be rendered. In our case, cells “under” the stack don’t need to be rendered, because they’re covered, so we use the hidden property.

When prepareLayout is called, we invoke prepareStacksLayout, an internal method to set up our instance variables. This method calculates the position for our stacks, and the code looks very familiar to what I think UICollectionViewFlow layout looks. Although this is a line-based, breaking layout, I don’t believe it could be implemented with UICollectionViewFlowLayout, because it deals with sections at a time instead of items (see Listing 5.25).

Listing 5.25 Calculating Section Stack Positions


- (void)prepareStacksLayout
{
self.numberOfStacks = [self.collectionView numberOfSections];
self.pageSize = self.collectionView.bounds.size;

CGFloat availableWidth =
self.pageSize.width - (self.stacksInsets.left +
self.stacksInsets.right);
self.numberOfStacksAcross =
floorf((availableWidth + self.minimumInterStackSpacing) /
(self.stackSize.width + self.minimumInterStackSpacing));
CGFloat spacing =
floorf((availableWidth - (self.numberOfStacksAcross *
self.stackSize.width)) / (self.numberOfStacksAcross - 1));
self.numberOfStackRows =
ceilf(self.numberOfStacks / (float)self.numberOfStacksAcross);

self.stackFrames = [NSMutableArray array];
int stackColumn = 0;
int stackRow = 0;
CGFloat left = self.stacksInsets.left;
CGFloat top = self.stacksInsets.top;

for (int stack = 0; stack < self.numberOfStacks; stack++)
{
CGRect stackFrame = (CGRect){{left, top}, self.stackSize};
[self.stackFrames addObject:[NSValue valueWithCGRect:stackFrame]];

left += self.stackSize.width + spacing;
stackColumn += 1;

if (stackColumn >= self.numberOfStacksAcross)
{
left = self.stacksInsets.left;
top +=
self.stackSize.height + STACK_FOOTER_GAP +
STACK_FOOTER_HEIGHT + self.minimumLineSpacing;
stackColumn = 0;
stackRow += 1;
}
}

self.contentSize =
CGSizeMake(self.pageSize.width,
MAX(self.pageSize.height,
self.stacksInsets.top + (self.numberOfStackRows *
(self.stackSize.height + STACK_FOOTER_GAP +
STACK_FOOTER_HEIGHT)) + ((self.numberOfStackRows - 1) *
self.minimumLineSpacing) + self.stacksInsets.bottom));
}


The relevant parts of layoutAttributesForItemAtIndexPath: are found in Listing 5.26. Again, note the use of the hidden property.

Listing 5.26 Calculating Item Attributes for a Stacked Layout


- (UICollectionViewLayoutAttributes
*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
CGRect stackFrame = [self.stackFrames[path.section] CGRectValue];

AFCollectionViewLayoutAttributes* attributes =
[AFCollectionViewLayoutAttributes
layoutAttributesForCellWithIndexPath:path];
attributes.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
attributes.center =
CGPointMake(CGRectGetMidX(stackFrame), CGRectGetMidY(stackFrame));
CGFloat angle = 0;

if (path.item == 1) angle = 5;
else if (path.item == 2) angle = -5;

attributes.transform3D =
CATransform3DMakeRotation(angle * M_PI / 180, 0, 0, 1);
attributes.alpha = path.item >= VISIBLE_ITEMS_PER_STACK? 0 : 1;
attributes.zIndex =
path.item >= VISIBLE_ITEMS_PER_STACK ?
0 : VISIBLE_ITEMS_PER_STACK - path.item;
attributes.hidden = path.item >= VISIBLE_ITEMS_PER_STACK;
attributes.shadowOpacity =
path.item >= VISIBLE_ITEMS_PER_STACK? 0 : 0.5;
}


The layoutAttributesForItemAtIndexPath: method is called by our layout-AttributesForElementsInRect, after it determines the index paths of the elements visible in that rect. This is typical of subclasses of UICollectionView itself; they need to calculate which items are in the rect because they can’t rely on super’s implementation, like with UICollectionViewFlowLayout. See Listing 5.27 for our implementation.

Listing 5.27 Calculating Item Index Paths in a Given rect


-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray* attributes = [NSMutableArray array];
for (int stack = 0; stack < self.numberOfStacks; stack++)
{
CGRect stackFrame = [self.stackFrames[stack] CGRectValue];
stackFrame.size.height +=
(STACK_FOOTER_GAP + STACK_FOOTER_HEIGHT);

if (CGRectIntersectsRect(stackFrame, rect))
{
NSInteger itemCount = [self.collectionView
numberOfItemsInSection:stack];
for (int item = 0; item < itemCount; item++)
{
NSIndexPath* indexPath = [NSIndexPath
indexPathForItem:item inSection:stack];
[attributes addObject:[self
layoutAttributesForItemAtIndexPath:indexPath]];
}
}
}

return attributes;
}


Our AFCollectionViewFlowLayout is incredibly simple, as shown in Listing 5.28.

Listing 5.28 Calculating Item Index Paths in a Given rect


@implementation AFCollectionViewFlowLayout

-(id)init
{
if (!(self = [super init])) return nil;

self.itemSize = CGSizeMake(200, 200);
self.sectionInset = UIEdgeInsetsMake(13.0f, 13.0f, 13.0f, 13.0f);
self.minimumInteritemSpacing = 13.0f;
self.minimumLineSpacing = 13.0f;

return self;
}

@end


Finally, our Cover Flow layout is copied directly from the previous chapter (see Listing 5.29). The only difference is the size of the items, which is specified in the controller.

Listing 5.29 Specifying the Item Size for the Cover Flow Layout


-(UIEdgeInsets)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
insetForSectionAtIndex:(NSInteger)section
{
if (collectionViewLayout == self.coverFlowLayout)
{
CGFloat margin = 0.0f;

if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation))
{
margin = 130.0f;
}
else
{
margin = 280.0f;
}

UIEdgeInsets insets = UIEdgeInsetsZero;

if (section == 0)
{
insets.left = margin;
}
else if (section == [collectionView numberOfSections] - 1)
{
insets.right = margin;
}

return insets;
}
else if (collectionViewLayout == self.flowLayout)
{
return self.flowLayout.sectionInset;
}
else
{
// Should never happen.
return UIEdgeInsetsZero;
}


This code might look a little perplexing. Unlike the Cover Flow example from the preceding chapter, we now have more than one section, so we need to use different insets for different sections. Figure 5.7 illustrates the problem. The diagram on top shows what would happen if each section had left and right edge insets set to margin; we would have a gap in between the orange and blue sections. Instead, we only want a left margin on the first section and a right margin on the last section.

Image

Figure 5.7 The need for different section insets

Because this delegate method belongs to UICollectionViewDelegateFlowLayout, it will only get called for our flow layout and our Cover Flow layout.

If we were to run the application right now, it would look like Figure 5.8.

Image

Figure 5.8 The stacked layout

That’s nice, but pretty boring. Let’s add code that will change layouts when we select an item, as shown in Listing 5.30.

Listing 5.30 Changing Layout on Cell Selection


-(void)collectionView:(UICollectionView *)collectionView
didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
if (self.collectionView.collectionViewLayout == self.stackLayout)
{
[collectionView deselectItemAtIndexPath:indexPath animated:NO];

[self.flowLayout invalidateLayout];
[self.collectionView
setCollectionViewLayout:self.flowLayout animated:YES];

[self.collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:
UICollectionViewScrollPositionCenteredVertically |
UICollectionViewScrollPositionCenteredHorizontally
animated:YES];

[self.navigationItem
setLeftBarButtonItem:[[UIBarButtonItem alloc]
initWithTitle:@"Back"
style:UIBarButtonItemStyleBordered
target:self
action:@selector(goBack)]
animated:YES];
}
else if (self.collectionView.collectionViewLayout == self.flowLayout)
{
[collectionView deselectItemAtIndexPath:indexPath animated:NO];

[self.coverFlowLayout invalidateLayout];
[self.collectionView
setCollectionViewLayout:self.coverFlowLayout animated:YES];
[self.collectionView
scrollToItemAtIndexPath:indexPath
atScrollPosition:
UICollectionViewScrollPositionCenteredVertically |
UICollectionViewScrollPositionCenteredHorizontally
animated:YES];
}
else
{
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
}
}

-(void)goBack
{
if (self.collectionView.collectionViewLayout == self.coverFlowLayout)
{
[self.flowLayout invalidateLayout];
[self.collectionView
setCollectionViewLayout:self.flowLayout
animated:YES];
}
else if (self.collectionView.collectionViewLayout == self.flowLayout)
{
[self.stackLayout invalidateLayout];
[self.collectionView
setCollectionViewLayout:self.stackLayout
animated:YES];

[self.navigationItem setLeftBarButtonItem:nil animated:YES];
}
}


The implementation of collectionView:didSelectItemAtIndexPath: should be fairly familiar. The only addition to our circle layout example from earlier is that we also need to scroll to the selected index, which could be offscreen after the change to our layout.UICollectionViewScrollPosition is a bit mask, so we can supply both a horizontal and vertical position; we choose center for both dimensions. We also add a “back” button when we’re not displaying the stacked layout. Figure 5.9 shows the transitions between the layouts.

Image

Figure 5.9 Our three layouts

Notice the lovely animations between the layout changes. That’s really awesome! And it comes with UICollectionView for free; we just had to scroll to our selected index after changing layout.

Now let’s contextualize our content a little more. When users first launch the app, they won’t know that the first section is Popular. We should add a supplementary view to let them know which section is which.

Create a new supplementary view class that contains a label, as shown in Listing 5.31.

Listing 5.31 Supplementary View for Stack Layout


static NSString *kind = @"AFCollectionViewHeaderView";

@interface AFCollectionViewHeaderView ()

@property (nonatomic, strong) UILabel *label;

@end

@implementation AFCollectionViewHeaderView

- (id)initWithFrame:(CGRect)frame
{
if (!(self = [super initWithFrame:frame])) return nil;

self.backgroundColor = [UIColor orangeColor];

self.label = [[UILabel alloc]
initWithFrame:CGRectMake(0, 0,
CGRectGetWidth(frame),
CGRectGetHeight(frame))];
self.label.autoresizingMask =
UIViewAutoresizingFlexibleHeight |
UIViewAutoresizingFlexibleWidth;
self.label.backgroundColor = [UIColor clearColor];
self.label.textAlignment = NSTextAlignmentCenter;
[self addSubview:self.label];

return self;
}

-(void)setText:(NSString *)text
{
self.label.text = text;
}

+(NSString *)kind
{
return kind;
}

@end


Register the supplementary view with the collection view in the controller’s loadView method (see Listing 5.32).

Listing 5.32 Supplementary View Registration


[collectionView registerClass:[AFCollectionViewHeaderView class]
forSupplementaryViewOfKind:[AFCollectionViewHeaderView kind]
withReuseIdentifier:HeaderIdentifier];


Now we need to provide context for the header view in the controller, as shown in Listing 5.33.

Listing 5.33 Supplementary Configuration


(UICollectionReusableView *)collectionView:
(UICollectionView *)collectionView
viewForSupplementaryElementOfKind:(NSString *)kind
atIndexPath:(NSIndexPath *)indexPath
{
AFCollectionViewHeaderView *headerView = [collectionView
dequeueReusableSupplementaryViewOfKind:kind
withReuseIdentifier:HeaderIdentifier
forIndexPath:indexPath];

switch (indexPath.section) {
case AFViewControllerPopularSection:
[headerView setText:@"Popular"];
break;
case AFViewControllerEditorsSection:
[headerView setText:@"Editors' Choice"];
break;
case AFViewControllerUpcomingSection:
[headerView setText:@"Upcoming"];
break;
}

return headerView;
}


Finally, we need to modify our stacked layout class to insert a supplementary view for each stack, as shown in Listing 5.34:

Listing 5.34 Supplementary View for Each Stack


-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray* attributes = [NSMutableArray array];
for (int stack = 0; stack < self.numberOfStacks; stack++)
{
CGRect stackFrame = [self.stackFrames[stack] CGRectValue];
stackFrame.size.height +=
(STACK_FOOTER_GAP + STACK_FOOTER_HEIGHT);
if (CGRectIntersectsRect(stackFrame, rect))
{
NSInteger itemCount = [self.collectionView
numberOfItemsInSection:stack];
for (int item = 0; item < itemCount; item++)
{
NSIndexPath* indexPath = [NSIndexPath
indexPathForItem:item inSection:stack];
[attributes addObject:[self
layoutAttributesForItemAtIndexPath:indexPath]];
}

// add small label as footer
[attributes addObject:[self
layoutAttributesForSupplementaryViewOfKind:
[AFCollectionViewHeaderView kind]
atIndexPath:[NSIndexPath
indexPathForItem:0 inSection:stack]]];
}
}

return attributes;
}

(UICollectionViewLayoutAttributes *)
layoutAttributesForSupplementaryViewOfKind:(NSString *)kind
atIndexPath:(NSIndexPath *)indexPath
{
if (![kind isEqualToString:[AFCollectionViewHeaderView kind]])
return nil;

UICollectionViewLayoutAttributes* attributes =
[UICollectionViewLayoutAttributes
layoutAttributesForSupplementaryViewOfKind:kind
withIndexPath:indexPath];

attributes.size =
CGSizeMake(STACK_WIDTH, STACK_FOOTER_HEIGHT);
CGRect stackFrame =
[self.stackFrames[indexPath.section] CGRectValue];
attributes.center =
CGPointMake(CGRectGetMidX(stackFrame),
CGRectGetMaxY(stackFrame) + STACK_FOOTER_GAP +
(STACK_FOOTER_HEIGHT/2));

return attributes;
}


Perfect. This places the header directly beneath each stack (see Figure 5.10). Let’s run the application.

Image

Figure 5.10 Stacked layout headers

That wraps up this chapter. UICollectionViewLayout offers a lot of power, but you have to do a lot of work to access that power. If you enjoyed this chapter, and I sincerely hope you have, then I think you’ll really like the next one. We’re going to look back at the examples through this book and add some interactivity to them.