Organizing Content with UICollectionViewFlowLayout - iOS UICollectionView: The Complete Guide, Second Edition (2014)

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

Chapter 4. Organizing Content with UICollectionViewFlowLayout

You now have the skills to use UICollectionView to display custom content to your users and can display cells as well as supplementary views. Up until now, we have focused on the actual content, not how it’s organized on the screen. This chapter explores how UICollectionViewis architected to use UICollectionViewLayout to organize its content. We take a close look at UICollectionViewFlowLayout and how subclassing it can get you a lot of customizability without a lot of extra work. We finish with a short history lesson as we exploreUITableView and how it’s related to UICollectionView.

What Is a Layout?

UICollectionViewLayout is an abstract class that should not be created itself; its only purpose is to be subclassed. Each collection view has a layout associated with it whose job it is to lay content out. Layouts are not concerned with the data contained in the views they lay out; they are only interested in their layout to the user.

UICollectionViewFlow is a direct subclass lays out content in line-based, line-breaking style. We’ve already seen UICollectionViewFlowLayout in its most basic form, a grid. We spend the rest of this chapter exploring the power that a simple flow layout subclass gives you as a developer. You can create astounding layouts with very little code, if you know where to put it.

A layout subclass has a few responsibilities. The collection view relies on its layout to tell it how to display its cells. This is a key concept: Layout content is not done by subclassing UICollectionView. Although this is a common pattern for layout subviews when subclassingUIScrollView, we’re going to avoid subclassing UICollectionView unless absolutely necessary.

So, a collection view asks its layout for clues about how to lay out its content. What’s the actual sequence of events that happens when a collection view displays content to a user?

First, the collection view interrogates its data source for information about the contents to be displayed to the user. This includes the number of sections and the number of items and supplementary views in each individual section.

Next, the collection view gathers information from its layout object about how to display the cells, supplementary views, and decoration views. This information is stored in instances of a class called UICollectionViewLayoutAttributes.

Finally, the collection view forwards information about the layout to the cells, supplementary views, and decoration views. Each of these classes is responsible for using the information it has been given to apply those layout attributes to itself. Deferring to the superclass’s implementation, or omitting an implementation entirely, will ensure that the layout attributes already handled by the collection view, like frame, are applied. Your implementations should concentrate on any custom attributes that you’ve added (but more on that later).

These steps occur whenever the existing layout is invalidated, which you can force by calling invalidateLayout on the layout object.

Now you are aware of the different classes used in laying content out: UICollection-View, which is the view that presents content to the user; UICollectionViewCell, which is responsible for displaying one unit of content to a user at a time; UICollectionViewLayout, which determines the attributes of items and returns that information to the collection view; and UICollectionViewLayoutAttributes, which is a class in which the layout stores information to be marshaled to the cells, supplementary views, and decoration views.

If you step back and look at these classes, a clear division exists between which ones are involved in data and their own layouts and those that are solely responsible for layout. Figure 4.1 shows the division: UICollectionView collects information about the data from the classes in the orange box and combines it with information about the layout from the classes in the blue box.

Image

Figure 4.1 Distinction between data and layout classes

Notice that the layout has an indirect reference to the delegate. This connection can be used by the layout to interrogate the delegate about information concerning the layout of specific items. For example, the UICollectionViewDelegateFlowLayout protocol extends theUICollectionViewDelegate and is used by UICollectionViewFlowLayout to ask the delegate about item-specific layout information. This topic is complicated, but you’ve already seen an example of this in the preceding chapter, when the delegate specified individual dimensions for different items. You’ll see an example later of extending this further.

We’ve covered the basics: what a layout is and what it does and how it interacts with the rest of the collection view architecture. This has been, so far, very academic. Let’s get to some code.

Subclassing UICollectionViewFlowLayout

We’ve already seen a lot of complex behavior and layouts be generated using the built-in UICollectionViewFlowLayout, so why would one choose to subclass it? There are a number of reasons:

Image To modify the attributes of the layout you’re subclassing beyond what is possible with delegate methods

Image To incorporate decoration views in your layout

Image To add new kinds of supplementary views

Image To extend UICollectionViewLayoutAttributes to add new attributes of items for your layout class to manage

Image To add gesture support

Image To customize the animation of insertion, update, and deletion updates to the collection view

With the exception of the gesture support, covered in Chapter 6, “Adding Interactivity to UICollectionView,” covers, we will look at code examples for each of the reasons to subclass flow layout.

Let’s look back at our Survey example—the code for which is in Better Survey. There are a few ways that we can make this better, and the first one is shown in Figure 4.2. Because not all of our cells have the same size, cells won’t be aligned vertically anymore. Out of the box,UICollectionViewFlowLayout does not provide support for the kind of “evenly spaced-out” feel that I think would look better here. Luckily, what we want falls under the “line-based, breaking layout” category of flow layouts, so I think we’ll be able to subclassUICollectionViewFlowLayout to accomplish the visual style we’re going for.

Image

Figure 4.2 Flow layout not aligning cells in a grid

I’m going to create a new file in Xcode and call it AFCollectionViewFlowLayout; it’s going to subclass UICollectionViewFlowLayout (see Listing 4.1). Next, we can move a lot of the layout logic out of the view controller into our layout.

Listing 4.1 AFCollectionViewFlowLayout Header File


#import <UIKit/UIKit.h>

#define kMaxItemDimension 200.0f
#define kMaxItemSize CGSizeMake(kMaxItemDimension, kMaxItemDimension)

@interface AFCollectionViewFlowLayout : UICollectionViewFlowLayout
@end


You can see that we’ve moved the maximum cell size into the header file for the layout. This is a more appropriate place for it than in the view controller.

Next, we’ll implement our own init method so we can set up our properties there (see Listing 4.2).

Listing 4.2 AFCollectionViewFlowLayout Initializer


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

self.sectionInset = UIEdgeInsetsMake(30.0f, 80.0f, 30.0f, 20.0f);
self.minimumInteritemSpacing = 20.0f;
self.minimumLineSpacing = 20.0f;
self.itemSize = kMaxItemSize;
self.headerReferenceSize = CGSizeMake(60, 70);

return self;
}


Finally, we need to change the creation of the layout object in the view controller. Use #import to import the AFCollectionViewFlowLayout header and change the creation of the layout and collection view to the code shown in Listing 4.3.

Listing 4.3 Simplified Layout and Collection View Creation


AFCollectionViewFlowLayout *surveyFlowLayout =
[[AFCollectionViewFlowLayout alloc] init];

UICollectionView *surveyCollectionView =


[[UICollectionView alloc]


initWithFrame:CGRectZero collectionViewLayout:surveyFlowLayout];


By moving the setup of the layout into its initializer, we’ve written a lot less code in the view controller. In addition, if we ever reuse the layout, we don’t have repeated code in two places. A view controller reusing this layout could always further customize the layout properties, but they don’t have to. This is a good pattern you should adhere to when writing your own custom layouts.

Next, we need to override two methods in our UICollectionViewFlowLayout subclass, which will be called when the collection view is laying out its cells, supplementary views, and decoration views. The two methods are layoutAttributesForElementsInRect: andlayoutAttributesForItemAtIndexPath:. We’re also going to have create a third, private method called applyLayoutAttributes:, which we discuss later. Both of the overridden methods will call this custom one (see Listing 4.4).

Listing 4.4 Applying Customized Attributes


-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *attributesArray = [super layoutAttributesForElementsInRect:rect];

for (UICollectionViewLayoutAttributes *attributes in attributesArray)
{
[self applyLayoutAttributes:attributes];
}

return attributesArray;
}

-(UICollectionViewLayoutAttributes *)
layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [super
layoutAttributesForItemAtIndexPath:indexPath];

[self applyLayoutAttributes:attributes];

return attributes;
}


The first thing both of our methods do is call their superclass’s implementation. By doing this, we get all of the UICollectionViewFlowLayout behavior for free. After we retrieve the default attributes, we’ll tweak them ourselves.

Now let’s look at applyLayoutAttributes:. We first check the layout attributes’ representedElementKind property. For normal UICollectionViewCells, this will be nil. Otherwise, it will be the supplementary view type registered with the collection view; in our case, it would be UICollectionElementKindSectionHeader.

One other point worth keeping in mind is that center and size define the position and size, respectively, of an item. When calculating these, you can end up rendering views on half-pixels, making them blurry. The frame property is a convenience method for accessing the size and center of the layout attributes. By setting the frame to be the CGRectIntegral of itself (see Listing 4.5), we ensure that views are not rendered on pixel boundaries.

Listing 4.5 Applying Customized Attributes


-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)
attributes
{
// Check for representedElementKind being nil, indicating this is
// a cell and not a header or decoration view
if (attributes.representedElementKind == nil)
{
CGFloat width = [self collectionViewContentSize].width;
CGFloat leftMargin = [self sectionInset].left;
CGFloat rightMargin = [self sectionInset].right;

NSUInteger itemsInSection = [[self collectionView]
numberOfItemsInSection:attributes.indexPath.section];
CGFloat firstXPosition =
(width - (leftMargin + rightMargin)) / (2 * itemsInSection);
CGFloat xPosition = firstXPosition +
(2*firstXPosition*attributes.indexPath.item);

attributes.center = CGPointMake(leftMargin + xPosition,
attributes.center.y);
attributes.frame = CGRectIntegral(attributes.frame);
}
}


Listing 4.5 is only a codified version of the formula laid out in Figure 4.3. It has been generalized to allow an arbitrary number of items in each row, instead of just three.

Image

Figure 4.3 Math to distribute items evenly

Ah, you knew there would be some math in this ebook eventually! But, it’s actually not that complicated.

If we were to run the application again, we would see that the cells are spread out evenly, as shown in Figure 4.4.

Image

Figure 4.4 Items evenly horizontally distributed

Now that we’ve got our cells laid out in a nice grid pattern, let’s add a decoration view. Decoration views are visual supplements to UICollectionView’s data-driven content. They don’t display information about the cells; instead, they accompany the cells for visual effect: a designer’s best friend.

I’m no designer, but I’ve managed to come up with the idea of a binder. Our app is going to flaunt this “flat design” craze and lay our photos on top of a three-ring binder. I’ve taken a photo of a binder and stretched it out. We want to have this decoration view lay behind each row of photos.

Because decoration views are not data driven, there will be no code added to the view controller. Instead, all the code for the decoration view will live inside our AFCollectionViewFlowLayout and a subclass of UICollectionReusableView. This class,UICollectionReusableView, is the parent class of AFCollectionHeaderView and even UICollectionViewCell. It provides common logic for reusing any particular view in a collection view, which includes cells, supplementary views, and decoration views. Because these classes can be reused, we can take what we’ve already learned about reuse and apply it to decoration views. Let’s do so now.

Create a new class that extends UICollectionReusableView. I call mine AFDecorationView. It has no properties, and its implementation looks rather boring (see Listing 4.6).

Listing 4.6 Decoration View Implementation


@implementation AFDecorationView
{
UIImageView *binderImageView;
}

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

binderImageView = [[UIImageView alloc]
initWithImage:[UIImage imageNamed:@"binder"]];
binderImageView.frame = CGRectMake(10, 0,
CGRectGetWidth(frame), CGRectGetHeight(frame));
binderImageView.contentMode = UIViewContentModeLeft;
binderImageView.autoresizingMask = UIViewAutoresizingFlexibleHeight |
UIViewAutoresizingFlexibleWidth;
[self addSubview:binderImageView];

return self;
}

@end


All this class does is, when initialized, adds a UIImageView to its view hierarchy with our “binder” image in it. There is no need to override prepareForReuse because there is no data-specific content in our decoration view.

Now that we have created our decoration view subclass, let’s add it to our collection view. This is a little trickier than the header views because nothing is built in to UICollectionView for us; we need to build everything ourselves.

#import the decoration view’s header file into the layout subclass. Modify the implementation of layoutAttributesForElementsInRect: to look like Listing 4.7.

Listing 4.7 Decoration View Implementation


-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *attributesArray = [super
layoutAttributesForElementsInRect:rect];

NSMutableArray *newAttributesArray = [NSMutableArray array];

for (UICollectionViewLayoutAttributes *attributes in attributesArray)
{
[self applyLayoutAttributes:attributes];

// THIS IF STATEMENT WAS ADDED
if (attributes.representedElementCategory ==
UICollectionElementCategorySupplementaryView)
{
UICollectionViewLayoutAttributes *newAttributes =

[self layoutAttributesForDecorationViewOfKind:
AFCollectionViewFlowLayoutBackgroundDecoration
atIndexPath:attributes.indexPath];

[newAttributesArray addObject:newAttributes];
}
}

attributesArray = [attributesArray
arrayByAddingObjectsFromArray:newAttributesArray];

return attributesArray;
}


The if statement checking the element category of the layout attributes was added. We want to add one decoration view per section, and each section has only one header, so we’ll piggy-back on that logic to add our supplementary view.

The code itself may look a little strange. Remember that layoutAttributesForElementsInRect: is called for all types of elements, not just cells. So, when it’s called for our header view, our if statement evaluates to YES and we create a new layout attribute. The array we return will include this new attribute.

Next, we need an implementation for layoutAttributesForDecorationViewOfKind: atIndexPath: because the default implementation returns nil, and when we try to add it to our mutable dictionary, our app would crash.

We need to implement a method that will create a new UICollectionViewLayoutAttributes object and customize its properties so that the decoration view would fit behind our cell contents (see Listing 4.8).

Listing 4.8 Creating Decoration View Layout Attributes


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

if ([decorationViewKind
isEqualToString:AFCollectionViewFlowLayoutBackgroundDecoration])
{
UICollectionViewLayoutAttributes *tallestCellAttributes;
NSInteger numberOfCellsInSection = [self.collectionView
numberOfItemsInSection:indexPath.section];

for (NSInteger i = 0; i < numberOfCellsInSection; i++)
{
NSIndexPath *cellIndexPath = [NSIndexPath
indexPathForItem:i
inSection:indexPath.section];

UICollectionViewLayoutAttributes *cellAttribtes = [self
layoutAttributesForItemAtIndexPath:cellIndexPath];

if (CGRectGetHeight(cellAttribtes.frame) >
CGRectGetHeight(tallestCellAttributes.frame))
{
tallestCellAttributes = cellAttribtes;
}
}

CGFloat decorationViewHeight =
CGRectGetHeight(tallestCellAttributes.frame) +
self.headerReferenceSize.height;

layoutAttributes.size = CGSizeMake(
[self collectionViewContentSize].width, decorationViewHeight);
layoutAttributes.center = CGPointMake(
[self collectionViewContentSize].width / 2.0f,
tallestCellAttributes.center.y);

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

return layoutAttributes;
}


This implementation creates a new UICollectionViewLayoutAttributes object using the class method layoutAttributesForDecorationViewOfKind:withIndexPath:. Then, it customizes the properties in the attributes depending on what we’re looking for. We want our decoration view to be vertically centered with the tallest item in its section, so we need to loop over each of those. Luckily, the logic to retrieve these attributes has already been implemented in layoutAttributesForItemAtIndexPath:. When we ask our super class for the attributes for a given cell, it will query the collection view delegate for the size (code we’ve already written).

We can leverage this existing functionality to handle the heavy lifting. We’re not calculating the center of the decoration view, really, we’re just relying on the vertical center of the tallest item, which has already been calculated for us. Hooray!

So, after we define the size and height of our decoration view, we need to set its z-index. This tells the collection view which order to render its items in. Items that overlap but have the same z-index have an undefined rendering order. We want the decoration view to render behind all the cells, which have the default z-index of 0, so we set our decoration view’s z-index to -1.

The only other thing we need to do is register our decoration view class with the layout (see Listing 4.9). We’ll add the highlighted following line to the AFCollectionViewFlowLayout’s init method.

Listing 4.9 Registering a Decoration View with a Layout


NSString * const AFCollectionViewFlowLayoutBackgroundDecoration =
@"DecorationIdentifier";

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

self.sectionInset = UIEdgeInsetsMake(30.0f, 80.0f, 30.0f, 20.0f);
self.minimumInteritemSpacing = 20.0f;
self.minimumLineSpacing = 20.0f;
self.itemSize = kMaxItemSize;
self.headerReferenceSize = CGSizeMake(60, 70);
// THIS LINE WAS ADDED!
[self registerClass:[AFDecorationView class]
forDecorationViewOfKind:AFCollectionViewFlowLayoutBackgroundDecoration];

return self;
}


Amazing! Figure 4.5 shows we’re nearly there. The final thing I think this demo app could use is some nice animations. Support for animations is already built into UICollection-ViewLayout; we just need to implement a few methods.

Image

Figure 4.5 Decoration views implemented

initialLayoutAttributesForAppearingItemAtIndexPath: is called whenever a new item is added or updated to the collection view. We can use it to supply initial layout attributes for the item at the beginning of the animation and the collection view will interpolate animatable properties, like frame and alpha, to their normal position. There is also a corresponding method called finalLayoutAttributesForDisappearingItemAtIndexPath: for animating removal of items from the collection view.

We can animate more than just items, though. There are corresponding appear/disappear methods for supplementary views and decoration views. The default implementation of UICollectionViewLayout returns nil, indicating a simple crossfade. We can also return nil to use a crossfade.

The remaining problem is that we reload other sections when we insert a new one. This will cause more than just the appearing sections to animate. Let’s limit which sections we animate.

Before any updates are performed to the collection view, prepareForCollectionView-Updates: is called with an array of UICollectionViewUpdateItem objects as a parameter. These are the updates that are about to happen. After they are completed, finalize-CollectionViewUpdates is called. These come in pairs. We’ll create an instance variable NSMutableSet to hold on to the sections that are being inserted. We use a set because it has constant-time lookup (see Listing 4.10).

Listing 4.10 Updated init Method to Create Mutable Set


@implementation AFCollectionViewFlowLayout
{
NSMutableSet *insertedSectionSet;
}

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

self.sectionInset = UIEdgeInsetsMake(30.0f, 80.0f, 30.0f, 20.0f);
self.minimumInteritemSpacing = 20.0f;
self.minimumLineSpacing = 20.0f;
self.itemSize = kMaxItemSize;
self.headerReferenceSize = CGSizeMake(60, 70);
[self registerClass:[AFDecorationView class]
forDecorationViewOfKind:AFCollectionViewFlowLayoutBackgroundDecoration];

// NOTICE THIS NEW SHINY LINE?
insertedSectionSet = [NSMutableSet set];

return self;
}


Now we just need implementations of prepareForCollectionViewUpdates: and finalizeCollectionViewUpdates to update the set. It is very important to always call your super implementation for these methods (see Listing 4.11).

Listing 4.11 Updating the Mutable Set Contents


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

[updateItems enumerateObjectsUsingBlock:^(UICollectionViewUpdateItem
*updateItem, NSUInteger idx, BOOL *stop) {
if (updateItem.updateAction == UICollectionUpdateActionInsert)
{
[insertedSectionSet
addObject:@(updateItem.indexPathAfterUpdate.section)];
}
}];
}

-(void)finalizeCollectionViewUpdates
{
[super finalizeCollectionViewUpdates];

[insertedSectionSet removeAllObjects];
}


You can see that, when we are preparing for updates, our layout checks the update action to see if it’s an item that’s being inserted. If so, it adds an NSNumber instance representing the item’s index path’s section to the set. Duplicates are ignored in sets, so we don’t have to check whether it already exists.

When the updates have been finalized, we remove all the items from the mutable set, resetting it to an empty state for the next batch of updates.

Now that we have that out of the way, let’s look at the code for animating in the items and the decoration view, which is shown in Listing 4.12.

Listing 4.12 Animating in Cells and Decoration Views


-(UICollectionViewLayoutAttributes *)
initialLayoutAttributesForAppearingDecorationElementOfKind:(NSString*)
elementKind atIndexPath:(NSIndexPath *)decorationIndexPath
{
//returning nil will cause a crossfade

UICollectionViewLayoutAttributes *layoutAttributes;

if ([elementKind
isEqualToString:AFCollectionViewFlowLayoutBackgroundDecoration])
{
if ([insertedSectionSet
containsObject:@(decorationIndexPath.section)])
{
layoutAttributes = [self
layoutAttributesForDecorationViewOfKind:elementKind
atIndexPath:decorationIndexPath];
layoutAttributes.alpha = 0.0f;
layoutAttributes.transform3D = CATransform3DMakeTranslation(
-CGRectGetWidth(layoutAttributes.frame), 0, 0);
}
}

return layoutAttributes;
}



-(UICollectionViewLayoutAttributes *)
initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)
itemIndexPath
{
//returning nil will cause a crossfade

UICollectionViewLayoutAttributes *layoutAttributes;

if ([insertedSectionSet containsObject:@(itemIndexPath.section)])
{
layoutAttributes = [self
layoutAttributesForItemAtIndexPath:itemIndexPath];
layoutAttributes.transform3D = CATransform3DMakeTranslation(
[self collectionViewContentSize].width, 0, 0);
}

return layoutAttributes;
}


Because the default implementations return nil, we don’t have to worry about calling super.

The two implementations are similar because they construct very similar animations. For decoration views, we check to make sure that the decoration view is the one we’ve set up; although there are no other decoration views, this is good practice in case we add more later.

In either case, we check to ensure that the index path’s section of the item is included in our set of sections that were inserted. If it is, we grab an instance of UICollectionView-LayoutAttributes from our earlier implementations of layoutAttributesForItemAt-IndexPath: or layoutAttributesForDecorationViewOfKind:atIndexPath: — we’re leveraging the code we’ve already written.

Then, we set up a transform to move the decoration view left and the cells right so that they are completely out of the visible collection view when the animation starts. We also set the decoration view’s alpha to zero so that it fades in.

Now, whenever a new section is inserted, the user sees the binder move in from the left and the photos move in from the right. This is a really nice touch.

One of the key architectural takeaways you should have from this section is that writing UICollectionViewFlowLayout subclasses is all about relying on existing code wherever possible. If you find yourself doing complex math to calculate something that’s already laid out, check to see whether there is some way you can access that information.

Laying Out Items with Custom Attributes

UICollectionViewLayoutAttributes is a class, which means we can subclass it. Why would we want to do that? To add support for more attributes, of course! Let’s look at what I mean.

The class contains the following properties, which are applied to items at runtime:

Image Frame (convenience property for center and size)

Image Center

Image Size

Image 3D Transform

Image Alpha (opacity)

Image Z-index

Image Hidden

Image Element category (cell, supplementary view, or decoration view)

Image Element kind (nil for cells)

These are all really great, and you can accomplish a lot with them as they are. However, sometimes, you might want to add your own.

That’s what we’re going to do now.

This project is called Dimensions in the sample code. It has some images and model setup done, which I do not cover here. The problem it’s trying to solve is that photos sometimes look best when stretched to aspect fill, clipping off the extra bits of the image to fit in its container. Other times, you want to use aspect fit, which will scale down the image so that the entire thing is visible within a container. We’re going to write a layout that will handle this for us as a layout attribute.

I’ve created a new Xcode project with the Single View application template. After removing the .xib, I changed the main window setup in the application delegate to look like Listing 4.13.

Listing 4.13 Setting Up the View Controller


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

self.viewController = navigationController;
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];

return YES;
}


All we’ve done is set up a navigation controller with a root custom view controller that Xcode created for us and that we’ll implement in a moment. Note that I had to change the type of the viewController property to be a generic UIViewController.

Now that we have our view controller onscreen, we can set up the collection view and layout (see Listing 4.14).

Listing 4.14 Setting Up the Collection View


@implementation AFViewController
{
//Array of model objects
NSArray *photoModelArray;

UISegmentedControl *aspectChangeSegmentedControl;

AFCollectionViewFlowLayout *photoCollectionViewLayout;
}

//Static identifier for cells
static NSString *CellIdentifier = @"CellIdentifier";

-(void)loadView
{
// Create our view

// Create an instance of our custom flow layout.
photoCollectionViewLayout = [[AFCollectionViewFlowLayout alloc] init];

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

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

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

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

// Set up our model
[self setupModel];
}


This should be familiar code to you by now. Note that we’ve disabled selection for all cells in the collection view. We also have a segmented control as an instance variable. This is going to go in the navigation bar so the user can select between aspect fit and aspect fill.

We’re going to implement the AFCollectionViewFlowLayout class referenced in loadView in a moment, but let’s look at the rest of the view controller code first. It sets up the segmented control in our navigation bar (see Listing 4.15).

Listing 4.15 Setting Up the Segmented Control


-(void)viewDidLoad
{
[super viewDidLoad];

aspectChangeSegmentedControl = [[UISegmentedControl alloc]
initWithItems:@[@"Aspect Fit", @"Square"]];
aspectChangeSegmentedControl.selectedSegmentIndex = 0;
aspectChangeSegmentedControl.segmentedControlStyle =
UISegmentedControlStyleBar;
[aspectChangeSegmentedControl addTarget:self
action:@selector(aspectChangeSegmentedControlDidChangeValue:)
forControlEvents:UIControlEventValueChanged];

self.navigationItem.titleView = aspectChangeSegmentedControl;
}


The rest of the view controller implementation is pretty standard (see Listing 4.16).

Listing 4.16 Boilerplate UICollectionViewController


-(AFPhotoModel *)photoModelForIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.item >= [photoModelArray count]) return nil;

return photoModelArray[indexPath.item];
}

-(void)configureCell:(AFCollectionViewCell *)cell forIndexPath:(NSIndexPath
*)indexPath
{
//Set the image for the cell
[cell setImage:[[self photoModelForIndexPath:indexPath] image]];
}

-(NSInteger)collectionView:(UICollectionView *)collectionView
numberOfItemsInSection:(NSInteger)section
{
return [photoModelArray count];
}

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

//Configure the cell
[self configureCell:cell forIndexPath:indexPath];

return cell;
}


The last remaining method in our view controller is going to be the method to respond to the user interacting with the segmented control (see Listing 4.17).

Listing 4.17 Responding to User Interaction with Segmented Control


-(void)aspectChangeSegmentedControlDidChangeValue:(id)sender
{
// We need to explicitly tell the collection view layout
// that we want the change animated.
[UIView animateWithDuration:0.5f animations:^{
// This just swaps the two values

if (photoCollectionViewLayout.layoutMode ==
AFCollectionViewFlowLayoutModeAspectFill)
{
photoCollectionViewLayout.layoutMode =
AFCollectionViewFlowLayoutModeAspectFit;
}
else
{
photoCollectionViewLayout.layoutMode =
AFCollectionViewFlowLayoutModeAspectFill;
}
}];
}


We haven’t defined the layoutMode property yet, so let’s do that now. This is where the custom layout attributes subclass comes in. We want to add a new layout attribute to specify the scaling mode of photos. Create a new class that subclasses UICollectionViewLayout-Attributes (see Listing 4.18).

Listing 4.18 UICollectionViewLayoutAttributes Subclass Header


typedef enum : NSUInteger{
AFCollectionViewFlowLayoutModeAspectFit, //Default
AFCollectionViewFlowLayoutModeAspectFill
}AFCollectionViewFlowLayoutMode;

@interface AFCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes

@property (nonatomic, assign) AFCollectionViewFlowLayoutMode layoutMode;

@end


That’s all we really need—a definition of layout modes and a property to hold them. However, look at the definition of UICollectionViewLayoutAttributes; notice that it conforms to the NSCopying protocol. It is very important that we also conform to this protocol and implement copyWithZone: (see Listing 4.19). Otherwise, our property will always be zero (as guaranteed by the compiler). New in iOS 7: You now must override isEqual: when subclassing layout attributes.

Listing 4.19 UICollectionViewLayoutAttributes Subclass Implementation


@implementation AFCollectionViewLayoutAttributes

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

attributes.layoutMode = self.layoutMode;

return attributes;
}

-(BOOL)isEqual:(id)object {
return [super isEqual:object] &&
(self.layoutMode == [object layoutMode]);
}

@end


Now we can implement our flow layout subclass. I’ve created a new class called AFCollectionViewFlowLayout that subclasses UICollectionViewFlowLayout. It’s shown in Listing 4.20 and should look familiar from the improved Survey app shown earlier in this chapter.

Listing 4.20 Custom Flow Layout Header


#import "AFCollectionViewLayoutAttributes.h"

#define kMaxItemDimension 140
#define kMaxItemSize CGSizeMake(kMaxItemDimension, kMaxItemDimension)

@protocol AFCollectionViewDelegateFlowLayout <UICollectionViewDelegateFlowLayout>

@optional
-(AFCollectionViewFlowLayoutMode)collectionView:(UICollectionView
*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout
layoutModeForItemAtIndexPath:(NSIndexPath *)indexPath;

@end

@interface AFCollectionViewFlowLayout : UICollectionViewFlowLayout

@property (nonatomic, assign) AFCollectionViewFlowLayoutMode layoutMode;

@end


What we’ve done is extend the UICollectionViewDelegateFlowLayout protocol to create our own. Just like we customized the size of individual cells for the Survey app, we want to provide an interface where developers using our layout can specify individual aspect ratios for the photos in their cells.

Now that we have our custom layout attributes class, let’s take a brief look at the parts of our custom layout that you should already be familiar with (see Listing 4.21).

Listing 4.21 Custom Flow Layout Implementation


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

// Some basic setup. 140+140 + 3*13 ~= 320, so we can get a
// two-column grid in portrait orientation.
self.itemSize = kMaxItemSize;
self.sectionInset = UIEdgeInsetsMake(13.0f, 13.0f, 13.0f, 13.0f);
self.minimumInteritemSpacing = 13.0f;
self.minimumLineSpacing = 13.0f;

return self;
}

-(void)applyLayoutAttributes:(AFCollectionViewLayoutAttributes *)attributes
{
// Check for representedElementKind being nil, indicating this
// is a cell and not a header or decoration view
if (attributes.representedElementKind == nil)
{
// Pass our layout mode onto the layout attributes
attributes.layoutMode = self.layoutMode;

if ([self.collectionView.delegate respondsToSelector:
@selector(collectionView:layout:
layoutModeForItemAtIndexPath:)])
{
attributes.layoutMode =
[(id<AFCollectionViewDelegateFlowLayout>)self.collectionView.delegate
collectionView:self.collectionView layout:self
layoutModeForItemAtIndexPath:attributes.indexPath];
}
}
}

-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *attributesArray = [super layoutAttributesForElementsInRect:rect];

for (AFCollectionViewLayoutAttributes *attributes in attributesArray)
{
[self applyLayoutAttributes:attributes];
}

return attributesArray;
}

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:
(NSIndexPath *)indexPath
{
AFCollectionViewLayoutAttributes *attributes =
(AFCollectionViewLayoutAttributes *)[super
layoutAttributesForItemAtIndexPath:indexPath];

[self applyLayoutAttributes:attributes];

return attributes;
}


This is the same kind of code we saw in our first flow layout subclass earlier in the chapter. The difference is that we’re using AFCollectionViewLayoutAttributes instead of UICollectionViewLayoutAttributes and we’re passing on our layoutMode.

In applyLayoutAttributes:, we check the collection view’s delegate to see whether it responds to the selector we defined in the AFCollectionViewDelegateFlowLayout protocol. If it does, we cast it to an id conforming to the protocol so we can grab the layout mode from it.

Observant readers might be asking themselves how the collection view knows to use our custom subclass of UICollectionViewLayoutAttributes. The answer is pretty easy. There is a class method our layout needs to implement that tells the collection view which custom class to use (see Listing 4.22). Obviously, the default implementation returns UICollectionViewLayoutAttributes.

Listing 4.22 Using a Custom Layout Attributes Class


+(Class)layoutAttributesClass
{
return [AFCollectionViewLayoutAttributes class];
}


The only other component missing is that our layout can end up in an invalid state. If we change our layout mode without updating the cells that are already laid out on the screen, cells that are already visible will have the old layout, whereas ones that become visible due to scrolling or insertion will have the new layout. What we need is to call invalidateLayout whenever our layout mode changes (see Listing 4.23).

Listing 4.23 Invalidating Layout in Overridden Setter


-(void)setLayoutMode:(AFCollectionViewFlowLayoutMode)layoutMode
{
// Update our backing ivar...
_layoutMode = layoutMode;

// then invalidate our layout.
[self invalidateLayout];
}


I know this has been a lot of code with no payoff, but bear with me a little longer. Even though we have our custom layout and are setting the custom property, we still don’t have any code that applies that property to the cell. I’ve created a UICollectionViewCell subclass calledAFCollectionViewCell. It displays the image set by its setImage: method. The implementation, shown in Listing 4.24, is almost identical to the one used in the Survey app from Chapter 3. However, two key differences exist.

First, we’re declaring an instance variable for the layout mode, and second, we’re using that instance variable in a new method that sets the image view’s frame. The issue is related to changes made under the hood in iOS 7; methods are now called in a different order, so it’s important to set the image’s frame whenever a new image is set (which makes sense because the image’s frame depends on the image’s aspect ratio, which we don’t know until we have the UIImage instance).

Listing 4.24 Standard Collection View Cell Displaying an Image


@implementation AFCollectionViewCell
{
UIImageView *imageView;
AFCollectionViewFlowLayoutMode layoutMode;
}


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

// Set up our image view
imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0,
CGRectGetWidth(frame), CGRectGetHeight(frame))];
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
imageView.clipsToBounds = YES;
[self.contentView addSubview:imageView];

// This will make the rest of our cell, outside the image view, appear transparent against a black background.
self.backgroundColor = [UIColor blackColor];

return self;
}

-(void)prepareForReuse
{
[super prepareForReuse];

[self setImage:nil];
}

#pragma mark - Public Methods

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

-(void)setImageViewFrame {
//start out with the detail image size of the maximum size
CGSize imageViewSize = self.bounds.size;

if (layoutMode == AFCollectionViewFlowLayoutModeAspectFit)
{
//Determine the size and aspect ratio for the model's image
CGSize photoSize = imageView.image.size;
CGFloat aspectRatio = photoSize.width / photoSize.height;

if (aspectRatio < 1)
{
//The photo is taller than it is wide, so constrain the width
imageViewSize = CGSizeMake(CGRectGetWidth(self.bounds) *
aspectRatio, CGRectGetHeight(self.bounds));
}
else if (aspectRatio > 1)
{
//The photo is wider than it is tall, so constrain the height
imageViewSize = CGSizeMake(CGRectGetWidth(self.bounds),
CGRectGetHeight(self.bounds) / aspectRatio);
}
}

// Set the size of the imageView ...
imageView.bounds = CGRectMake(0, 0,
imageViewSize.width, imageViewSize.height);
// And the center, too.
imageView.center = CGPointMake(CGRectGetMidX(self.bounds),
CGRectGetMidY(self.bounds));
}


@end


Importantly, the image view’s clipsToBounds property is set to YES. This makes sure that when the photo is being scaled to fit within the image view and clips part of itself, the clipped regions won’t be visible.

Next, we have the code to actually apply the layout mode to the cell (see Listing 4.25).

Listing 4.25 Applying Custom Layout Attributes


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

// Important! Check to make sure we're actually this special subclass.
// Failing to do so could cause the app to crash!
if (![layoutAttributes isKindOfClass:[AFCollectionViewLayoutAttributes
class]])
{
return;
}

AFCollectionViewLayoutAttributes *castedLayoutAttributes =
(AFCollectionViewLayoutAttributes *)layoutAttributes;


layoutMode = castedLayoutAttributes.layoutMode;

[self setImageViewFrame];
}


This method belongs to UICollectionReusableView because layout attributes are applicable to cells, supplementary views, and decoration views. First, you must call super’s implementation. Next, it checks to ensure that the layout attributes are an instance of our custom subclass before casting the pointer.

We use the layout mode to determine if we should leave the image view’s size set to our bounds size, or if we should adjust it. If the mode is aspect fit, we adjust it using similar logic to the survey view controller in Chapter 3, “Contextualizing Content.” Finally, we set the bounds and the center of the image view. We use the size and position instead of the contentMode so that we can easily animate the transition from one mode to another. (bounds and center are implicitly animatable properties.)

Finally, after all that code, you can run the app and transition between aspect fit and aspect fill photos (see Figure 4.6). It will animate the transition, even if animating a scroll or rotation.

Image

Figure 4.6 Aspect fit and aspect fill layout modes

Going Beyond Grids

So far, all we’ve seen flow layout do is some variation on a grid. While a grid is, indeed, a line-based, breaking layout, it is just one specific case of such a layout. Let’s take things further and do something really fun.

We’re going to build a Cover Flow layout. Before we do, I want to especially thank Mark Pospesel for building his Introducing Collection Views project on GitHub. The code in this section of my book draws heavily upon his examples, used with his permission. The sample code for this section is available under the name of Cover Flow.

The first thing we’ll do, after our standard “create a single-view Xcode project and remove the .xib file” is to open the project settings in the project navigator pane. Under Build Phases, expand Link Binary with Libraries and click the plus sign. Select QuartzCore and open up the Prefix file under the Supporting Files group. Mine is called CoverFlow-Prefix.pch; it’s a header file that’s imported into all header files. Add #import <QuartzCore/QuartzCore.h> to the PCH. Now we have access to all of QuartzCore all throughout the project. We’ll need this later to useCALayer. This is such a common step for me in creating Xcode projects; it’s a wonder that Apple doesn’t include it by default.

The view controller is going to be very similar to the Dimensions, except this time we’ll have two layouts. We’re going to use a segmented control in the navigation bar, like last time, to switch between these two layouts (see Listing 4.26).

Listing 4.26 Creating Two Layouts


@implementation AFViewController
{
//Array of selection objects
NSArray *photoModelArray;

UISegmentedControl *layoutChangeSegmentedControl;

AFCoverFlowFlowLayout *coverFlowCollectionViewLayout;
UICollectionViewFlowLayout *boringCollectionViewLayout;
}

//Static identifiers for cells and supplementary views
static NSString *CellIdentifier = @"CellIdentifier";

-(void)loadView
{
//Create our view

// Create our awesome cover flow layout
coverFlowCollectionViewLayout = [[AFCoverFlowFlowLayout alloc] init];

boringCollectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
boringCollectionViewLayout.itemSize = CGSizeMake(140, 140);
boringCollectionViewLayout.minimumLineSpacing = 10.0f;
boringCollectionViewLayout.minimumInteritemSpacing = 10.0f;

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

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

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

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

// Set up our model
[self setupModel];
}

-(void)viewDidLoad
{
[super viewDidLoad];

// Crate a segmented control to sit in our navigation bar
layoutChangeSegmentedControl = [[UISegmentedControl alloc]
initWithItems:@[@"Boring", @"Cover Flow"]];
layoutChangeSegmentedControl.selectedSegmentIndex = 0;
layoutChangeSegmentedControl.segmentedControlStyle =
UISegmentedControlStyleBar;
[layoutChangeSegmentedControl
addTarget:self
action:@selector(layoutChangeSegmentedControlDidChangeValue:)
forControlEvents:UIControlEventValueChanged];

self.navigationItem.titleView = layoutChangeSegmentedControl;
}


The data source methods for configuring the collection view are identical to those used in the preceding section, so I do not include them here. However, we are going to implement a new UICollectionViewDelegateFlowLayout method that will be responsible for returning the edge insets for our layouts (see Listing 4.27). We use this approach because the Cover Flow layout requires different section edge insets, depending on the orientation of the interface and the specific device its running on. I like to keep this kind of logic out of theUICollectionViewLayout subclass, if possible.

Listing 4.27 Custom Section Insets


-(UIEdgeInsets)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
insetForSectionAtIndex:(NSInteger)section
{
if (collectionViewLayout == boringCollectionViewLayout)
{
// A basic flow layout that will accommodate three
// columns in portrait
return UIEdgeInsetsMake(10, 20, 10, 20);
}
else
{
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation))
{
// Portrait is the same in either orientation
return UIEdgeInsetsMake(0, 70, 0, 70);
}
else
{
// We need to get the height of the main screen to see
// if we're running on a 4" screen. If so, we need
// extra side padding.
if (CGRectGetHeight([[UIScreen mainScreen] bounds]) > 480)
{
return UIEdgeInsetsMake(0, 190, 0, 190);
}
else
{
return UIEdgeInsetsMake(0, 150, 0, 150);
}
}
}
}


These values were determined mainly by experimentation to see what looked right. I would encourage you to take this approach, instead of divining them mathematically, for the simple reason that it doesn’t matter if something is mathematically correct if it doesn’t look correct to your users.

Finally, we need to implement our user interaction code. Shown in Listing 4.28, you’ll notice it is similar to the last example.

Listing 4.28 Changing Layouts


-(void)layoutChangeSegmentedControlDidChangeValue:(id)sender
{
// Change to the alternate layout

if (layoutChangeSegmentedControl.selectedSegmentIndex == 0)
{
[self.collectionView
setCollectionViewLayout:boringCollectionViewLayout
animated:NO];
}
else
{
[self.collectionView
setCollectionViewLayout:coverFlowCollectionViewLayout
animated:NO];
}

// Invalidate the new layout
[self.collectionView.collectionViewLayout invalidateLayout];
}


We explicitly do not animate the change in layout because they are so different that the animation between them looks jarring to the user. As you’ll see in the next chapter, changing between layouts with animation is actually pretty easy to do.

After changing the layout, we need to invalidate the new layout. Although this is not included in the documentation, I’ve noticed some strange behavior on some layouts if you omit it. Experiment to see what works for your custom layouts.

We’re going to create a new custom UICollectionViewLayoutAttributes subclass to hold two values: one to indicate whether we should rasterize the layer, and the other to indicate how “masked out” the cell should appear. We can’t use alpha because cells behind the semitransparent ones would “bleed through.” The new subclass is shown in Listing 4.29. For our cover view layout, cells will always be rasterized because otherwise they get some jagged edges due to their 3D transform.

As for the masking layer, we want items that are not at the center of the collection view to not be as prominent, so we’ll place a semitransparent mask view over top of each cell.

Listing 4.29 Custom Layout Attributes Class for Cover Flow


// .h file

@interface AFCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes

@property (nonatomic, assign) BOOL shouldRasterize;
@property (nonatomic, assign) CGFloat maskingValue;

@end

// .m file

@implementation AFCollectionViewLayoutAttributes

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

attributes.shouldRasterize = self.shouldRasterize;
attributes.maskingValue = self.maskingValue;

return attributes;
}

-(BOOL)isEqual:(AFCollectionViewLayoutAttributes *)other {
return [super isEqual:other] && (self.shouldRasterize ==
other.shouldRasterize
&& self.maskingValue == other.maskingValue);
}

@end


Next, let’s look at the custom UICollectionViewFlowLayout subclass itself (see Listing 4.30). I omitted the #defines at the top of the file that are used later. I’ll include them there.

Listing 4.30 Custom Layout Attributes Class for Cover Flow


@implementation AFCoverFlowFlowLayout

#pragma mark - Overridden Methods

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

// Set up our basic properties
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.itemSize = CGSizeMake(180, 180);

// Gets items up close to one another
self.minimumLineSpacing = -60;

// Makes sure we only have 1 row of items in portrait mode
self.minimumInteritemSpacing = 200;

return self;
}

+(Class)layoutAttributesClass
{
return [AFCollectionViewLayoutAttributes class];
}

-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)oldBounds
{
// Very important — needed to re-layout the cells when scrolling.
return YES;
}

-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray* layoutAttributesArray = [super
layoutAttributesForElementsInRect:rect];

// We're going to calculate the rect of the collection view visisble to the
user.
CGRect visibleRect = CGRectMake(
self.collectionView.contentOffset.x,
self.collectionView.contentOffset.y,
CGRectGetWidth(self.collectionView.bounds),
CGRectGetHeight(self.collectionView.bounds));

for (UICollectionViewLayoutAttributes* attributes in layoutAttributesArray)
{
// We're going to calculate the rect of the collection
// view visible to the user.
// That way, we can avoid laying out cells that are not visible.
if (CGRectIntersectsRect(attributes.frame, rect))
{
[self applyLayoutAttributes:attributes forVisibleRect:visibleRect];
}
}

return layoutAttributesArray;
}

- (UICollectionViewLayoutAttributes
*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [super
layoutAttributesForItemAtIndexPath:indexPath];

// We're going to calculate the rect of the collection view visible
// to the user.
CGRect visibleRect = CGRectMake(
self.collectionView.contentOffset.x,
self.collectionView.contentOffset.y,
CGRectGetWidth(self.collectionView.bounds),
CGRectGetHeight(self.collectionView.bounds));

[self applyLayoutAttributes:attributes forVisibleRect:visibleRect];

return attributes;
}


Most of this is standard-looking flow layout code. However, notice that we are calculating the visible rectangle in the collection view. This rectangle is going to be used to determine how much 3D transform and translation to apply to each cell. We’ll calculate it easily by getting the content offset and bounds size of the collection view.

We also return YES in shouldInvalidateLayoutForBoundsChange so that when the user scrolls, the transforms of the cells are recalculated (at every frame refresh).

The minimumLineSpacing is negative because we want our cells to be “bunched up” close together, and in horizontally scrolling collection views, the line spacing is the distance between each vertical column of cells. As you can see in Figure 4.7, the line space is calculated as the space between the lines and the inter-item spacing is the space in between the cells along the line.

Image

Figure 4.7 Difference between line space and inter-item spacing depending on scroll direction

It can be tricky to wrap your head around, so remember that in vertically scrolling collection views, line spacing and inter-item spacing are analogous to line height and kerning in writing, respectively. In horizontally scrolling collection views, they are flipped.

Next up is the intensive math used to apply the perspective 3D transform to our cells (see Listing 4.31). (Again, I need to thank Mark Pospesel for his help.)

Listing 4.31 Cover Flow Layout Math


#define ACTIVE_DISTANCE 100
#define TRANSLATE_DISTANCE 100
#define ZOOM_FACTOR 0.2f
#define FLOW_OFFSET 40
#define INACTIVE_GREY_VALUE 0.6f

-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes
forVisibleRect:(CGRect)visibleRect
{
// Applies the cover flow effect to the given layout attributes.

// We want to skip supplementary views.
if (attributes.representedElementKind) return;

// Calculate the distance from the center of the visible rect to the
// center of the attributes. Then normalize it so we can compare them
// all. This way, all items further away than the active get the same
// transform.
CGFloat distanceFromVisibleRectToItem =
CGRectGetMidX(visibleRect) - attributes.center.x;

CGFloat normalizedDistance =
distanceFromVisibleRectToItem / ACTIVE_DISTANCE;

// Handy for use in making a number negative selectively
BOOL isLeft = distanceFromVisibleRectToItem > 0;

// Default values
CATransform3D transform = CATransform3DIdentity;
CGFloat maskAlpha = 0.0f;

if (fabsf(distanceFromVisibleRectToItem) < ACTIVE_DISTANCE)
{
// We're close enough to apply the transform in relation to
// how far away from the center we are.

transform = CATransform3DTranslate(
CATransform3DIdentity,
(isLeft? - FLOW_OFFSET : FLOW_OFFSET)*
ABS(distanceFromVisibleRectToItem/TRANSLATE_DISTANCE),
0,
(1 - fabsf(normalizedDistance)) * 40000 + (isLeft? 200 : 0));

// Set the perspective of the transform.
transform.m34 = -1/(4.6777f * self.itemSize.width);

// Set the zoom factor.
CGFloat zoom = 1 + ZOOM_FACTOR*(1 - ABS(normalizedDistance));
transform = CATransform3DRotate(transform,
(isLeft? 1 : -1) * fabsf(normalizedDistance) *
45 * M_PI / 180,
0,
1,
0);
transform = CATransform3DScale(transform, zoom, zoom, 1);
attributes.zIndex = 1;

CGFloat ratioToCenter = (ACTIVE_DISTANCE -
fabsf(distanceFromVisibleRectToItem)) / ACTIVE_DISTANCE;
// Interpolate between 0.0f and INACTIVE_GREY_VALUE
maskAlpha = INACTIVE_GREY_VALUE + ratioToCenter *
(-INACTIVE_GREY_VALUE);
}
else
{
// We're too far away - just apply a standard
// perspective transform.

transform.m34 = -1/(4.6777 * self.itemSize.width);
transform = CATransform3DTranslate(transform,
isLeft? -FLOW_OFFSET : FLOW_OFFSET, 0, 0);
transform = CATransform3DRotate(transform, (
isLeft? 1 : -1) * 45 * M_PI / 180, 0, 1, 0);
attributes.zIndex = 0;

maskAlpha = INACTIVE_GREY_VALUE;
}

attributes.transform3D = transform;

// Rasterize the cells for smoother edges.
[(AFCollectionViewLayoutAttributes *)attributes
setShouldRasterize:YES];
[(AFCollectionViewLayoutAttributes *)attributes
setMaskingValue:maskAlpha];
}


Phew! Don’t worry if it seems like a lot. I’ll go through the high-level details, and you can experiment around with the specifics later; this isn’t a book about CATransform3D, after all. The important thing to know is that you can apply a transform in three dimensions with collection views. Cool!

The first if branch executes if the attribute’s item is close enough to the center of the visible area. It will give it a zoom, translation, and a 3D perspective transform depending on how close it is to the center. If an item is exactly at the center, the transform does nothing.

The else branch executes if the item is far enough away from the center to make sure items don’t become too transformed. Imagine Cover Flow where the items extending to the edges kept having more and more transform applied; they would eventually become so transformed that they would flip around to their other sides!

We also want to set up a default mask value of zero and always set the rasterization to YES. Let’s run the app now to see what’s happening. Notice that you can switch between the plain flow layout and the Cover Flow layout really easily (see Figure 4.8).

Image

Figure 4.8 In-progress Cover Flow implementation

It looks great. However, there are a few things wrong with this. First, notice that the collection view is stopped halfway between cells; in the real Cover Flow, the scroll view comes to rest with an item perfectly centered. Next, you can clearly see that our layout attributes for masking and rasterization are not being applied. Hmm. Oh, that’s because we haven’t written the code to do that, yet. Let’s deal with the first problem, first.

targetContentOffsetForProposedContentOffset:withScrollingVelocity: is a method defined in UICollectionViewLayout and is available to be overridden by subclasses, including ours. It provides an opportunity for subclasses to define where the collection view will “snap” to. We’re going to implement it and use our existing code in layoutAttributesForElementsInRect: to get the attributes for the elements in the proposed rect (see Listing 4.32). Then, we’ll find the attribute whose item will be closest to the center of the proposed visible rect. Then, we’ll find out how far away that item will be and return an adjusted content offset that centers that view.

Listing 4.32 Stopping on Cells


-(CGPoint)
targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
withScrollingVelocity:(CGPoint)velocity
{
// Returns a point where we want the collection view to stop
// scrolling at. First, calculate the proposed center of the
// collection view once the collection view has stopped
CGFloat offsetAdjustment = MAXFLOAT;
CGFloat horizontalCenter = proposedContentOffset.x +
(CGRectGetWidth(self.collectionView.bounds) / 2.0);

// Use the center to find the proposed visible rect.
CGRect proposedRect = CGRectMake(
proposedContentOffset.x,
0.0,
self.collectionView.bounds.size.width,
self.collectionView.bounds.size.height);

// Get the attributes for the cells in that rect.
NSArray* array = [self
layoutAttributesForElementsInRect:proposedRect];

// This loop will find the closest cell to proposed center
// of the collection view.
for (UICollectionViewLayoutAttributes* layoutAttributes in array)
{
// We want to skip supplementary views
if (layoutAttributes.representedElementCategory !=
UICollectionElementCategoryCell)
continue;

// Determine if this layout attribute's cell is closer than
// the closest we have so far
CGFloat itemHorizontalCenter = layoutAttributes.center.x;
if (fabsf(itemHorizontalCenter - horizontalCenter) <
fabsf(offsetAdjustment))
{
offsetAdjustment = itemHorizontalCenter - horizontalCenter;
}
}

return CGPointMake(proposedContentOffset.x + offsetAdjustment,
proposedContentOffset.y);
}


Now, our app will snap to the nearest item. Let’s implement our UICollectionViewCell subclass next. Listing 4.33 has the complete implementation, but the important method is applyLayoutAttributes:.

Listing 4.33 Cover Flow Cell Implementation


@implementation AFCollectionViewCell
{
UIImageView *imageView;
UIView *maskView;
}

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

// Set up our image view
imageView = [[UIImageView alloc] initWithFrame:
CGRectInset(CGRectMake(0,
0,
CGRectGetWidth(frame),
CGRectGetHeight(frame)),
10, 10)];
imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
imageView.clipsToBounds = YES;
[self.contentView addSubview:imageView];

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

// This will make the rest of our cell, outside the image view, appear
transparent against a black background.
self.backgroundColor = [UIColor whiteColor];

return self;
}

#pragma mark - Overridden Methods

-(void)prepareForReuse
{
[super prepareForReuse];

[self setImage:nil];
}

-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
[super applyLayoutAttributes:layoutAttributes];
maskView.alpha = 0.0f;
self.layer.shouldRasterize = NO;

// Important! Check to make sure we're actually this special subclass.
// Failing to do so could cause the app to crash!
if (![layoutAttributes isKindOfClass:[AFCollectionViewLayoutAttributes
class]])
{
return;
}

AFCollectionViewLayoutAttributes *castedLayoutAttributes =
(AFCollectionViewLayoutAttributes *)layoutAttributes;

self.layer.shouldRasterize = castedLayoutAttributes.shouldRasterize;
maskView.alpha = castedLayoutAttributes.maskingValue;
}

#pragma mark - Public Methods

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

@end


Now we can run the application and see the effect of “fading out” the other cells and the snap-to effect (see Figure 4.9).

Image

Figure 4.9 Final Cover Flow implementation

Nice! Play around with it. Experiment with rotation and changing layouts while the collection view is decelerating. Find its capabilities and limitations.

Now that implementation is complete, I want to talk about a few things that I found problematic with collection views.

First, rotation animation on the Cover Flow view isn’t perfect. I can’t seem to get it seamless; I think it might have something to do with changing contentSize during rotation.

I originally tried changing the layout to Cover Flow during rotation so that the normal flow layout would be used in portrait and the Cover Flow layout would be used in landscape. Changing layouts during rotation was very problematic because the contentSize is not reliable in the layout subclass during rotation and even more unreliable when changing layouts.

I researched these problems and found the precise order of events when a layout is used:

1. prepareLayout is called on the layout so it has an opportunity to perform any upfront computations.

2. collectionViewContentSize is called on the layout to determine the collection view’s content size.

3. layoutAttributesForElementsInRect: is called.

Then, the layout becomes live and continues to call layoutAttributesForElements-InRect: and layoutAttributesForItemAtIndexPath: until the layout becomes invalidated. Then, the process is repeated again.

Using the content size in a layout is probably not a good idea; UICollectionView is still very new, and the community is still determining the best practices for working with it.

Depending on your idea for a layout, it might be best to turn to UICollectionView-Layout, as we do the next chapter. However, always consider whether UICollectionViewFlowLayout can accomplish your goals first. It does a lot of heavy lifting for you.

We’ve now covered decoration views, collection view layouts, layout attributes, and custom animations. You’ve solidified your knowledge from the first three chapters and dipped your toes into the water for the upcoming chapter. We’re on the cusp of doing some really interesting stuff, but first, let’s take a look back at UITableView.

UITableView: UICollectionView’s Daddy

UICollectionView was only introduced in iOS 6, but UITableView has been around since the original iPhone SDK was released in 2008. Many of the same principles used with UITableView apply to UICollectionView, but some have been modified.

UITableView has only recently started to use the class registration method to create its cells. This is the only way to do so with collection views.

Table view “batch updates” are done by calling a method to start the updates, performing them, and then calling another method to indicate that the updates are over. Collection views, however, only offer the (better, in my opinion) block-based performBatchUpdates: method.

Those are some minute differences in the way developers accomplish their goals with the classes. A much bigger philosophical difference between the two classes is that table view cells handle a lot of their internal layout. This starkly contrasts to collection view cells, which handle none at all. This forces developers to implement their own UITableViewCell subclasses from the ground up, every time. Meanwhile, UITableViewCell has four different “styles” that define how its two text labels, image view, “accessory” view, and editing style are laid out. Quite the difference!

I believe that if Apple were to introduce UITableView today, knowing what they’ve learned about framework design in the past 6 years, UITableViewCell would not have styles at all. Instead, they would have a few direct subclasses that developers could use, or they could implement their own subclasses.

Even though UITableView appears bloated by the standards of a modern Objective-C framework, UICollectionViews owes a lot of its sleekness to the lessons Apple has learned since originally crafting UITableView.