New to iOS 6, collection views present organized grids that lay out cells. These collections go well beyond standard table views and their vertically scrolling lists of cells. Collection views use many of the same concepts as tables but provide more power and more flexibility. With collection views, you create side-scrolling lists, grids, one-of-a-kind layouts like circles, and more. Plus, this new class offers integrated visual effects through layout specifications and lots of great features like snapping into place after scrolling.
As with tables, you can add an enormous range of implementation details to collection views. This chapter introduces you to the basics: to the collection view, its client sources, its special-purpose controller, and its cells. You read about how to develop standard and customized collections, how you can start adding special effects to your presentations, and how you can take advantage of the built-in animation support to create the most effective interaction possible.
That said, collection views are more powerful than any single chapter can properly cover. This chapter offers fundamental collection view concepts. From here, how sharp you hone your collection view is up to you.
UICollectionView
instances present an ordered collection of data items. Like table views, collections are made up of cells, headers, and footers powered by data sources and delegates. Unlike tables, collections introduce a layout, a class that specifies how items are placed onscreen. Layouts organize the location of each cell, so items appear exactly where needed.
Table 11-1 compares these two layout families. As you see, each family offers a core view class and a prebuilt controller class. These classes rely on a data source, which feeds cells on demand and provides other content information. They use a delegate to respond to user interactions.
There are also several fundamental differences, starting with the humble index path. Both classes are organized by section as their primary grouping, with each section containing indexed individual cells. Because collection views can scroll either direction, vertical or horizontal, terminology has changed. Table views use sections and rows; collection views use sections and items. The NSIndexPath
class has been updated in iOS 6 to reflect this new scheme.
Collection views introduce a new kind of content called “decoration” views, which provide visual enhancements like backdrops. This new class understands that cells and scrolling is just the starting point for the class. That you’ll want to customize the entire look to create coherent presentations using any metaphor you can imagine. Collection views also rethink headers and footers, transforming those into supplementary views with a little more API flexibility than those found in tables.
Expect a few practical differences between building table views and collection views. Collection views are less tolerant of lazy data loading. As a rule, when you create a collection view, make sure the data source that powers that view is fully prepared to go—even if it’s prepared with a minimal or empty set of cells as you load data elsewhere in your application.
You cannot wait until your initialization, loadView
or viewDidLoad
methods, to prepare content. Get those ready and going first, whether in your application delegate or before you instantiate and add your collection view or push a new child collection view controller. If your data is not ready to go, your app will crash; this is not the user experience you should be aiming toward.
Make sure you fully establish your collection view’s layout object before presenting the collection. As you’ll see in recipes in this chapter, you set up all layout details, including the scroll direction and any properties that don’t rely on delegate callbacks. Only then, create and initialize your collection view, as shown here:
MyCollectionController *mcc = [[MyCollectionController alloc]
initWithCollectionViewLayout:layout];
Passing a nil
layout produces an exception.
As with tables, collections come in two flavors: views and prebuilt controllers. You either build an individual collection view instance and add it to your presentation or use a UICollectionViewController
object that offers a view controller prepopulated with a collection view. The controller automatically sets the view’s data source, delegate, and layout delegate to itself and declares all three protocols. Embed the collection view controller as a child of any container (such as navigation controllers, tab bar controllers, split view controllers, page view controllers, and so on) or present it on its own.
Like table views, collection views have delegate
and dataSource
properties. The layout delegate protocol (UICollectionViewDelegateFlowLayout
) uses the object assigned to the delegate
property.
To build a controller, create and set up a layout, allocate the new instance, and initialize it with the prepared layout:
UICollectionViewFlowLayout *layout =
[[UICollectionViewFlowLayout alloc] init];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
MyCollectionController *mcc = [[MyCollectionController alloc]
initWithCollectionViewLayout:layout];
This snippet used a collection view flow layout in its default form, only setting the scroll direction. As you’ll see through this chapter, you can do a lot more with layouts. Typically, you set additional properties or subclass system-supplied layouts and add your own behavior.
As a rule, you use the UICollectionViewFlowLayout
class. It’s the layout workhorse for collection views. Use it to build any basic presentation. In its default form, each section automatically wraps items to fit the screen, and you can specify how much space appears between sections, between lines, between items, and so forth. It’s insanely customizable, as you’ll see in the next section, which details many tweaks you can apply to flow layouts.
Its parent class, UICollectionViewLayout
, offers an abstract base class for subclassing (which you mostly don’t; nearly every time, you’ll want to subclass the flow layout version instead) and isn’t meant for direct use.
When looking at subclassing layouts, refer to UICollectionViewLayout
. The parent of the UICollectionViewFlowLayout
class, its documentation provides the canonical list of customizable methods.
To create a collection view for embedding into another view, establish a layout, create the view using the layout and set the data source and delegate. The flow layout delegate defaults to the object you set as the delegate property:
UICollectionViewFlowLayout *layout =
[[UICollectionViewFlowLayout alloc] init];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero
collectionViewLayout:layout];
collectionView.dataSource = self;
collectionView.delegate = self;
View controllers coordinating collection views declare UICollectionViewDataSource
and UICollectionViewDelegate
. Unlike table views, the software development kit (SDK) introduces a third protocol for collection views, which is UICollectionViewDelegateFlowLayout
.
The delegate flow layout protocol coordinates layout information with your collection’s layout instance through a series of callbacks. Your collection view’s delegate
adopts this protocol—that is, you do not have to specify a third collection view property like delegateFlowLayout
or anything like that.
As with table views, the data source provides section and item information and returns cells and other collection view items on demand. The delegate handles user interactions and provides meaningful responses to user changes. The flow layout delegate introduces section-by-section layout details and is, for the most part, completely optional. You read about flow layouts and their delegate callbacks in the next section.
Flow layouts provided by the UICollectionViewFlowLayout
class create organized grid presentations in your application. They provide built-in properties that you edit directly or establish via delegate callbacks. These properties specify how the flow sets itself up to place items onscreen. In its most basic form, the layout properties provide you with a geometric vocabulary, where you talk about row spacing, indentation, and item-to-item margins.
The scrollDirection
property controls whether sections are lined up horizontally (UICollectionViewScrollDirectionHorizontal
) or vertically (UICollectionViewScrollDirectionVertical
). Figure 11-1 demonstrates otherwise identical layouts with horizontal (left) and vertical (right) flows. The members of each grouped section wrap to available space based on the current flow. Because there is more vertical space than horizontal space in the iPhone portrait presentation, section groups are longer and thinner in the horizontal flow than the vertical flow.
Use the itemSize
property to specify the default size for each onscreen item, like the small squares in Figure 11-1. The minimumLineSpacing
and minimumInteritemSpacing
properties specify how much space you need wrapped between objects within each section. Line spacing always goes between each line in the direction of flow. For example, line spacing refers to the space between S0(0) and S0(6) in Figure 11-1 (left) or between S0(0) and S0(4) in Figure 11-1 (right). Item spacing is orthogonal (at right angles) to lines, specifying the gap to leave between each item, such as between S0(0) and S0(1), and between S0(1) and S0(2).
Figure 11-2 shows these properties in action, in this case using a vertical flow. The left figure shows consistent spacing of 10 points. The middle figure expands line spacing to 30 points. This space appears between lines of items, where the flow wraps from one line to the next. The right figure expands item spacing to 30 points. Item spaces appear along each row, adding spacers between each object.
As with many new layout items in iOS 6, these settings are requests. Specifically, the spacing may exceed whatever value you specify, but the layout tries to respect the minimums you assign.
You can set the mentioned layout properties directly to assign default values applied across an entire collection. You can also use flow layout delegate callback methods to specify values from code. Setting these values at runtime offers far more nuance than the default settings, as they are applied on a section-by-section and item-by-item basis rather than globally. The following methods handle item size and minimum spacing:
• collectionView:layout:sizeForItemAtIndexPath:
corresponds to the itemSize
property, on an item-by-item basis.
• collectionView:layout:minimumLineSpacingForSectionAtIndex:
corresponds to the minimumLineSpacing
property, but controls it on a section-by-section basis.
• collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
corresponds to the minimumInteritemSpacing
property, again on a section-by-section basis.
Of these, the first method for item sizes offers the adaptation most typically used in iOS development. It enables you to build collections whose items, unlike those shown in Figure 11-2, vary in dimension. Figure 11-4, which follows later in this chapter, shows a flow layout that adjusts itself to multisized contents.
The headerReferenceSize
and footerReferenceSize
properties define how wide or how high header and footer items should be. Notice the difference between the extents for these items in Figure 11-3 in the top two and bottom two screen shots. The horizontal flow at the top uses 60-point wide spacing for these two items. The vertical flow at the bottom uses 30-point high spacing. Although you supply a full CGSize
to these properties, the layout uses only one field at any time based on the flow direction. For horizontal flow, it’s the width field; for vertical, it’s the height.
Here are the two callbacks used to generate the Figure 11-3 layouts. They return complete size structures even though only one field is used at any time. There are no corresponding properties in the flow layout class for these methods:
- (CGSize) collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
referenceSizeForHeaderInSection:(NSInteger)section
{
return CGSizeMake(60.0f, 30.0f);
}
- (CGSize) collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
referenceSizeForFooterInSection:(NSInteger)section
{
return CGSizeMake(60.0f, 30.0f);
}
The two minimum spacing properties define how each in-section item relates to other items within a section. In contrast, the sectionInset
property describes how the outer edges of a section add padding. This padding affects how sections relate to their optional headers and footers, and how sections move apart from each other in general.
Edge insets consist of a set of {top, left, bottom, right} values. Figure 11-3 shows how this works with collection views. Each shot in Figure 11-3 presents a flow using the same edge insets of 50 points at the top, 30 points at the bottom, and 10 points left and right:
UIEdgeInsetsMake(50.0f, 10.0f, 30.0f, 10.0f)
The top row shows a horizontal flow, the bottom row a vertical flow. In each case, you see how the insets affect layout. In the horizontal flow, the headers and footers adjust vertically to allow for the top spacing. In the vertical flow, the extra space happens below any headers and above any footers. Similarly, the left and right spacing are incorporated between headers and footers in the horizontal flow and adjust the entire layout in vertical flow.
Be aware that this layout behavior changed significantly during the beta rollout and is a likely candidate for updates in future iOS releases. When in doubt, test your layouts to ensure their behavior matches what you specified.
Recipe 11-1 introduces a basic collection view controller implementation, with support for optional headers and footers. This recipe implements the essential data source and delegate methods you need for a simple grid-based flow layout. You can modify the source to adjust the number of sections to be viewed, the items per section, and any other layout details that control the overall flow.
You control whether a collection view uses headers and footers by implementing the first two reference size requests in Recipe 11-1. These are the one for “header in section” and the one for “footer in section.” You’ll find these two methods just after the Flow Layout bookmark. Returning a zero size to the header or footer flow delegate method tells the collection view to omit those features for the section in question. When you return any other size, the data source moves on to requesting the supplementary elements for either a header or footer.
Make sure to register all cell and supplementary view classes before using them in your data source. Recipe 11-1 registers its classes in its viewDidLoad
method. Once registered, you can dequeue instances on demand. In the iOS 6 SDK, you no longer have to check whether a dequeuing request returns a usable instance. The methods create and initialize instances for you when needed.
I encourage you to dive into the sample code for Recipe 11-1 and tweak each layout value and callback, as I did to create the figures you’ve already seen in this section, to see how they affect overall flow and appearance. Recipe 11-1 offers a great jumping off point for testing collection views and seeing how each property influences the final presentation.
@implementation TestBedViewController
#pragma mark Flow Layout
- (CGSize) collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
referenceSizeForHeaderInSection:(NSInteger)section
{
return useHeaders ? CGSizeMake(60.0f, 30.0f) : CGSizeZero;
}
- (CGSize) collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
referenceSizeForFooterInSection:(NSInteger)section
{
return useFooters ? CGSizeMake(60.0f, 30.0f) : CGSizeZero;
}
#pragma mark Data Source
// Number of sections total
- (NSInteger)numberOfSectionsInCollectionView:
(UICollectionView *)collectionView
{
return 10;
}
// Number of items per section
- (NSInteger)collectionView:(UICollectionView *)collectionView
numberOfItemsInSection:(NSInteger)section
{
return 12;
}
// Dequeue and prepare a cell
- (UICollectionViewCell *)collectionView:
(UICollectionView *)aCollectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [self.collectionView
dequeueReusableCellWithReuseIdentifier:@"cell"
forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
cell.selectedBackgroundView = [[UIView alloc] initWithFrame:CGRectZero];
cell.selectedBackgroundView.backgroundColor =
[[UIColor blackColor] colorWithAlphaComponent:0.5f];
return cell;
}
// If using headers and footers, dequeue and prepare a view
- (UICollectionReusableView *)collectionView:
(UICollectionView *)aCollectionView
viewForSupplementaryElementOfKind:(NSString *)kind
atIndexPath:(NSIndexPath *)indexPath
{
if (kind == UICollectionElementKindSectionHeader)
{
UICollectionReusableView *header = [self.collectionView
dequeueReusableSupplementaryViewOfKind:
UICollectionElementKindSectionHeader
withReuseIdentifier:@"header" forIndexPath:indexPath];
header.backgroundColor = [UIColor blackColor];
return header;
}
if (kind == UICollectionElementKindSectionFooter)
{
UICollectionReusableView *footer = [self.collectionView
dequeueReusableSupplementaryViewOfKind:
UICollectionElementKindSectionFooter
withReuseIdentifier:@"footer" forIndexPath:indexPath];
footer.backgroundColor = [UIColor darkGrayColor];
return footer;
}
return nil;
}
#pragma mark Delegate methods
- (void)collectionView:(UICollectionView *)aCollectionView
didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"Selected %@", indexPath);
}
- (void)collectionView:(UICollectionView *)aCollectionView
didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"Deselected %@", indexPath);
}
#pragma mark Setup
- (void) viewDidLoad
{
// Register any cell and header/footer classes for re-use queues
[self.collectionView
registerClass:[UICollectionViewCell class]
forCellWithReuseIdentifier:@"cell"];
[self.collectionView
registerClass:[UICollectionReusableView class]
forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
withReuseIdentifier:@"header"];
[self.collectionView
registerClass:[UICollectionReusableView class]
forSupplementaryViewOfKind:UICollectionElementKindSectionFooter
withReuseIdentifier:@"footer"];
self.collectionView.backgroundColor = [UIColor lightGrayColor];
// Allow users to select/deselect items by tapping
self.collectionView.allowsMultipleSelection = YES;
}
@end
// From the application delegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UICollectionViewFlowLayout *layout =
[[UICollectionViewFlowLayout alloc] init];
layout.sectionInset = UIEdgeInsetsMake(10.0f, 10.0f, 50.0f, 10.0f);
layout.minimumLineSpacing = 10.0f;
layout.minimumInteritemSpacing = 10.0f;
layout.itemSize = CGSizeMake(50.0f, 50.0f);
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
TestBedViewController *tbvc = [[TestBedViewController alloc]
initWithCollectionViewLayout:layout];
UINavigationController *nav = [[UINavigationController alloc]
initWithRootViewController:tbvc];
window.rootViewController = nav;
[window makeKeyAndVisible];
return YES;
}
To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-6-Cookbook and go to the folder for Chapter 11.
Recipe 11-1 created uniformly sized objects, but there’s no reason your collections cannot be filled with items of any dimension. Flow layouts allow you to create far more nuanced presentations, as shown in Figure 11-4. Recipe 11-2 adapts its collection view to provide this juiced-up presentation by creating custom cells. These cells add image views, and the image’s size powers the “size for item at index path” callback to the collection view’s data source:
- (CGSize) collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
UIImage *image = artDictionary[indexPath];
return image.size;
}
To create custom cells, subclass UICollectionViewCell
and add any new views to the cell’s contentView
. This recipe adds a single image view subview, and exposes it through an imageView
property. When providing cells, the delegate adds custom images to the image view and the layout delegate specifies their sizes.
@interface ImageCell : UICollectionViewCell
{
UIImageView *imageView;
}
@property (nonatomic) UIImageView *imageView;
@end
@implementation ImageCell
- (id) initWithFrame:(CGRect)frame
{
if (![super initWithFrame:frame]) return nil;
_imageView = [[UIImageView alloc] initWithFrame:(CGRect){
.origin = CGPointMake(4.0f, 4.0f),
.size=CGRectInset(frame, 4.0f, 4.0f).size}];
_imageView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.contentView addSubview:_imageView];
return self;
}
@end
To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-6-Cookbook and go to the folder for Chapter 11.
Collection views offer the ability to create horizontal scrolling lists, a counter point to table views that only scroll vertically. To accomplish this, you need to take a few things into account, primarily that flow layouts in their default state naturally wrap their sections. Consider Figure 11-5. It shows two collection views, both of which scroll horizontally. The top image consists of a single section with 100 items; the bottom has 100 sections of a single item each.
You could force the top layout not to wrap by adding large left and right section margins, but getting these to work correctly is messy; the margins depend on both device and orientation. Assigning one item per section is a much easier solution and ensures a single line of items outside any issues of size.
Recipe 11-3 creates a horizontally scrolling collection as a stand-alone view rather than as a view controller. This approach allows the view to be inset as a subview, neatly avoiding the big empty area at the bottom of the screen shown in Figure 11-5 (bottom).
This recipe’s InsetCollectionView
class provides its own data source and exposes its collection view as a readonly property to allow clients to provide delegation. Figure 11-6 shows this recipe in action, providing an embedded horizontally scrolling list.
Recipe 11-8, which appears later in this chapter, introduces a fully customized layout subclass that offers true grid layouts. Recipe 11-3 offers a handy shortcut for anyone who wants to use the default flow layout, as shipped. Plus, it demonstrates how to create a collection view outside the context of a prebuilt controller.
@interface InsetCollectionView : UIView
<UICollectionViewDataSource>
{
UICollectionView *collectionView;
}
@property (strong, readonly) UICollectionView *collectionView;
@end
@implementation InsetCollectionView
// 100 sections of 1 item each
- (NSInteger)numberOfSectionsInCollectionView:
(UICollectionView *)collectionView
{
return 100;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView
numberOfItemsInSection:(NSInteger)section
{
return 1;
}
// This is a little utility that returns a view showing the
// section and item numbers for an index path
- (UIImageView *) viewForIndexPath: (NSIndexPath *) indexPath
{
NSString *string = [NSString stringWithFormat:
@"S%d(%d)", indexPath.section, indexPath.item];
UIImage *image = blockStringImage(string, 16.0f);
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
return imageView;
}
// Return an initialized cell
- (UICollectionViewCell *)collectionView:(UICollectionView *)_collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [self.collectionView
dequeueReusableCellWithReuseIdentifier:@"cell"
forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
cell.selectedBackgroundView =
[[UIView alloc] initWithFrame:CGRectZero];
cell.selectedBackgroundView.backgroundColor =
[[UIColor blackColor] colorWithAlphaComponent:0.5f];
// Show the section and item in a custom subview
if ([cell viewWithTag:999])
[[cell viewWithTag:999] removeFromSuperview];
UIImageView *imageView = [self viewForIndexPath:indexPath];
imageView.tag = 999;
[cell.contentView addSubview:imageView];
return cell;
}
#pragma mark Setup
- (id) initWithFrame:(CGRect)frame
{
if (!([super initWithFrame:frame])) return nil;
// Setup horizontal layout
UICollectionViewFlowLayout *layout =
[[UICollectionViewFlowLayout alloc] init];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
layout.sectionInset = UIEdgeInsetsMake(10.0f, 10.0f, 40.0f, 10.0f);
layout.minimumLineSpacing = 10.0f;
layout.minimumInteritemSpacing = 10.0f;
layout.itemSize = CGSizeMake(100.0f, 100.0f);
// Create collection view
collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero
collectionViewLayout:layout];
collectionView.backgroundColor = [UIColor darkGrayColor];
collectionView.dataSource = self;
// Register cells
[collectionView registerClass:[UICollectionViewCell class]
forCellWithReuseIdentifier:@"cell"];
return self;
}
@end
To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-6-Cookbook and go to the folder for Chapter 11.
Flow layouts are fully controllable. When subclassing UICollectionViewFlowLayout
, you gain immediate real-time control over how items are sized and placed onscreen. This provides incredible power to you as a developer letting you specify item presentation with great delicacy. You can use this power to develop flows that seem to work in three dimensions, or ones that break the linear mold and transform columns and rows into circles, piles, Bezier curves, and more.
Customizable layout attributes include standard layout elements (frame
, center
, and size
), transparency (alpha
and hidden
), position on the z-axis (zIndex
), and transform (transform3d
). You adjust these when the flow layout requests element attributes, as demonstrated in Recipe 11-4.
This recipe creates a flow that zooms items out towards the user in the center of the screen, and shrinks them as they move away to the left or right. It calculates how far away each item is from the horizontal center of the screen. It applies its scaling based on a cosine function (that is, one that maxes out as the distance from the center decreases).
Figure 11-7 shows this effect, although it’s much better to run the recipe yourself and see the changes in action.
@interface PunchedLayout : UICollectionViewFlowLayout
@end
@implementation PunchedLayout
// Allow the presentation to resize as needed
- (BOOL)shouldInvalidateLayoutForBoundsChange: (CGRect) oldBounds
{
return YES;
}
// Layout elements
- (NSArray *)layoutAttributesForElementsInRect: (CGRect) rect
{
// Retrieve the default layout
NSArray *array = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes* attributes in array)
{
// Only handle layouts for visible items
if (!CGRectIntersectsRect(attributes.frame, rect)) continue;
// Calculate the distance from the view center
CGSize boundsSize = self.collectionView.bounds.size;
CGFloat midX = boundsSize.width / 2.0f;
CGPoint contentOffset = self.collectionView.contentOffset;
CGPoint itemCenter = CGPointMake(
attributes.center.x - contentOffset.x,
attributes.center.y - contentOffset.y);
CGFloat distance = ABS(midX - itemCenter.x);
// Normalize the distance and calculate the zoom factor
CGFloat normalized = distance / midX;
normalized = MIN(1.0f, normalized);
CGFloat zoom = cos(normalized * M_PI_4);
// Set the transform
attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0f);
}
return array;
}
@end
To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-6-Cookbook and go to the folder for Chapter 11.
Because Recipe 11-4 focuses user attention at the center of the screen, why not ensure that the central object moves to the most optimal position? You accomplish this by implementing a layout method that snaps to specific boundaries. Recipe 11-5 shows how.
This targetContentOffsetForProposedContentOffset:
method, which is called during scrolling, specifies where the scroll would naturally stop. It iterates through all the onscreen objects, finds the one closest to the view’s horizontal center, and adjusts the offset so that object’s center coincides with the view’s.
- (CGPoint) targetContentOffsetForProposedContentOffset: (CGPoint)
proposedContentOffset
withScrollingVelocity: (CGPoint) velocity
{
CGFloat offsetAdjustment = MAXFLOAT;
// Retrieve all onscreen items at the proposed starting point
CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0,
boundsSize.width, boundsSize.height);
NSArray *array = [super layoutAttributesForElementsInRect:targetRect];
// Determine the proposed center x-coordinate
CGFloat proposedCenterX = proposedContentOffset.x + midX;
// Search for the minimum offset adjustment
for (UICollectionViewLayoutAttributes* layoutAttributes in array)
{
CGFloat distance = layoutAttributes.center.x - proposedCenterX;
if (ABS(distance) < ABS(offsetAdjustment))
offsetAdjustment = distance;
}
// Offset the content by the minimal centering
return CGPointMake(proposedContentOffset.x + offsetAdjustment,
proposedContentOffset.y);
}
To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-6-Cookbook and go to the folder for Chapter 11.
Circle layouts offer an eye-catching way to arrange views around a central area. Recipe 11-6 is heavily based on Apple’s sample code, which was first presented at WWDC 2012. This layout provides an excellent introduction to the way items can animate into place upon creation and deletion.
Recipe 11-6’s layout flow uses a fixed content size via the collectionViewContentSize
method. This prevents collection view scrolling as it creates a layout area with well-understood geometry. The code further limits its layout to an inset area, calculated in the prepareLayout
method. The height or width of the screen, whichever is currently smaller, determines the circle’s radius. This remains fixed regardless of device orientation.
The layout calculates each item’s position by its index path. This presentation uses a single section, and the order of the item within that section (that is, whether it is the third or fifth item) sets its progress along the circle:
CGFloat progress = (float) path.item / (float) numberOfItems;
CGFloat theta = 2.0f * M_PI * progress;
You can easily extend this to any shape or path whose progress can be normalized within the range [0.0, 1.0]. For a circle, this goes from 0 to 2 Pi. A spiral might go out 3, 4, or even 5 Pi. For a Bezier curve, you’d iterate along whatever control points define the curve and interpolate between them as needed.
Of particular interest in Recipe 11-6 are the methods that specify the initial attributes for newly inserted items and final attributes for newly deleted ones. These properties allow your collection views to animate item creation and deletion from the previous layout to the new layout after those items have been added or removed.
In this recipe, as in Apple’s original sample code, new items start off transparent in the center of the circle and fade into view as they move out to their assigned position. Deleted items shrink, fade, and move to the center. When you run the sample code, you’ll see these animations take effect.
As of late iOS 6 betas, the starting and ending attribute requests are called on all items, not just the added and deleted ones. Because of this, Recipe 11-6 sorts items into collections: added index paths and deleted index paths. It limits its custom insertion and deletion attributes to those items.
This mechanism offers a way to animate layout attributes for all items, enabling you to add extra animations as needed. For example, you might animate an object moving from the end of row 3 to the start of row 4 as a new item is inserted into row 3. This approach allows you to animate the cell off screen to the right of row 3 and then onscreen from the left of row 4 versus the default behavior, which has it move diagonally from its old position to the new one.
Unfortunately, this recipe is subject to future changes; it appears that Apple has not fully finished designing layout classes.
I made a number of changes to Apple’s original sample in putting together this recipe. For one thing, Recipe 11-6 uses Add and Delete bar buttons rather than gestures. For another, each view is distinct and identifiable by its color. Instead of deleting “any item” or adding “some item,” Recipe 11-6 uses selections. The user chooses an item to focus on. That selection controls which item is deleted (the selected item) or where new items should be added (just after the selected item).
Here is the deletion code. It retrieves the currently selected item, deletes it, and selects the next item. Then it enables or disables the Add and Delete buttons depending on how many items are currently onscreen:
- (void) delete
{
if (!count) return;
// Decrement the count
count -= 1;
// Determine which item to delete
NSArray *selectedItems = [self.collectionView indexPathsForSelectedItems];
NSInteger itemNumber = selectedItems.count ?
((NSIndexPath *)selectedItems[0]).item : 0;
NSIndexPath *itemPath = [NSIndexPath indexPathForItem:itemNumber inSection:0];
// Perform deletion
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:@[itemPath]];
} completion:^(BOOL done){
if (count)
[self.collectionView selectItemAtIndexPath:
[NSIndexPath indexPathForItem:MAX(0, itemNumber - 1) inSection:0]
animated:NO scrollPosition:UICollectionViewScrollPositionNone];
self.navigationItem.rightBarButtonItem.enabled = (count > 0);
self.navigationItem.leftBarButtonItem.enabled =
(count < (IS_IPAD ? 20 : 8));
}];
}
In the real world, there are very few use-cases for adding and deleting interchangeable views, but there are many for views that have meaning. These changes provide a more solid jumping-off point for extending this recipe to practical applications.
Figure 11-8 shows the layout built by Recipe 11-6. As users add new items, the circle grows more crowded, up to a maximum count of 20 items on the iPad and 8 on the iPhone. You can easily modify these limits in the add
and delete
methods to match the view sizes for your particular application.
@implementation CircleLayout
- (void) prepareLayout
{
[super prepareLayout];
CGSize size = self.collectionView.frame.size;
numberOfItems = [self.collectionView numberOfItemsInSection:0];
centerPoint = CGPointMake(size.width / 2.0f, size.height / 2.0f);
radius = MIN(size.width, size.height) / 3.0f;
}
// Fix the content size to the frame size
- (CGSize) collectionViewContentSize
{
return self.collectionView.frame.size;
}
// Calculate position for each item
- (UICollectionViewLayoutAttributes *)
layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
UICollectionViewLayoutAttributes *attributes =
[UICollectionViewLayoutAttributes
layoutAttributesForCellWithIndexPath:path];
CGFloat progress = (float) path.item / (float) numberOfItems;
CGFloat theta = 2.0f * M_PI * progress;
CGFloat xPosition = centerPoint.x + radius * cos(theta);
CGFloat yPosition = centerPoint.y + radius * sin(theta);
attributes.size = [self itemSize];
attributes.center = CGPointMake(xPosition, yPosition);
return attributes;
}
// Calculate layouts for all items
- (NSArray *) layoutAttributesForElementsInRect: (CGRect) rect
{
NSMutableArray *attributes = [NSMutableArray array];
for (NSInteger index = 0 ; index < numberOfItems; index++)
{
NSIndexPath *indexPath =
[NSIndexPath indexPathForItem:index inSection:0];
[attributes addObject:
[self layoutAttributesForItemAtIndexPath:indexPath]];
}
return attributes;
}
// Build insertion and deletion collections from updates
- (void)prepareForCollectionViewUpdates: (NSArray *)updates
{
[super prepareForCollectionViewUpdates:updates];
for (UICollectionViewUpdateItem* updateItem in updates)
{
if (updateItem.updateAction == UICollectionUpdateActionInsert)
[insertedIndexPaths
addObject:updateItem.indexPathAfterUpdate];
else if (updateItem.updateAction ==
UICollectionUpdateActionDelete)
[deletedIndexPaths
addObject:updateItem.indexPathBeforeUpdate];
}
}
// Establish starting attributes for added item
- (UICollectionViewLayoutAttributes *)
insertionAttributesForItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes *attributes =
[self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
attributes.center = centerPoint;
return attributes;
}
// Establish ending attributes for deleted item
- (UICollectionViewLayoutAttributes *)
deletionAttributesForItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
UICollectionViewLayoutAttributes *attributes =
[self layoutAttributesForItemAtIndexPath:itemIndexPath];
attributes.alpha = 0.0;
attributes.center = centerPoint;
attributes.transform3D = CATransform3DMakeScale(0.1, 0.1, 1.0);
return attributes;
}
// Handle insertion animation for all items
- (UICollectionViewLayoutAttributes*)
initialLayoutAttributesForAppearingItemAtIndexPath:
(NSIndexPath*)indexPath
{
return [insertedIndexPaths containsObject:indexPath] ?
[self insertionAttributesForItemAtIndexPath:indexPath] :
[super initialLayoutAttributesForAppearingItemAtIndexPath:
indexPath];
}
// Handle deletion animation for all items
- (UICollectionViewLayoutAttributes*)
finalLayoutAttributesForDisappearingItemAtIndexPath:
(NSIndexPath*)indexPath
{
return [deletedIndexPaths containsObject:indexPath] ?
[self deletionAttributesForItemAtIndexPath:indexPath] :
[super finalLayoutAttributesForDisappearingItemAtIndexPath:
indexPath];
}
@end
To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-6-Cookbook and go to the folder for Chapter 11.
Recipe 11-7 builds on Recipe 11-6 by adding interactive gestures that adjust presentation layout. It uses two recognizers, a pinch recognizer and a rotation recognizer, to enable users to scale and rotate the circle of views. These items are set up to recognize simultaneously, so users can pinch and rotate at the same time.
The rotate recognizer uses a slightly more sophisticated approach than the pinch one. Unlike pinch values, rotations are relative. You rotate by an amount, not to a specific angle. To accommodate this, Recipe 11-7 implements callbacks to handle two states. The first is called as rotations happen, updating the presentation to match each movement. The second resets the rotation baseline as the gesture ends, so the next interaction will take up where the last left off:
- (void) pinch: (UIPinchGestureRecognizer *) pinchRecognizer
{
CircleLayout *layout =
(CircleLayout *)self.collectionView.collectionViewLayout;
[layout scaleTo:pinchRecognizer.scale];
[layout invalidateLayout];
}
- (void) rotate: (UIRotationGestureRecognizer *) rotationRecognizer
{
CircleLayout *layout =
(CircleLayout *)self.collectionView.collectionViewLayout;
if (rotationRecognizer.state == UIGestureRecognizerStateEnded)
[layout rotateTo:rotationRecognizer.rotation];
else
[layout rotateBy:rotationRecognizer.rotation];
[layout invalidateLayout];
}
Notice how these callbacks invalidate the layout so that the presentation is updated in real time. This recipe is best tested on-device due to the high graphical load.
Recipe 11-7 calculates the effect of user gestures on the layout by adjusting the view radius (it scales from a minimum of 0.5 to a maximum of 1.3 times the original layout) and the layout’s start angle, which is initially at 0 degrees but is adjusted each time the rotation updates. The scaled radius and the adjusted angle value form the basis for the new presentation.
// Intermediate rotation
- (void) rotateBy: (CGFloat)theta { currentRotation = theta; }
// Final rotation
- (void) rotateTo: (CGFloat) theta
{
rotation += theta;
currentRotation = 0.0f;
}
// Scaling
- (void) scaleTo: (CGFloat) factor
{
scale = factor;
}
// Calculate position for each item
- (UICollectionViewLayoutAttributes *)
layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
UICollectionViewLayoutAttributes *attributes =
[UICollectionViewLayoutAttributes
layoutAttributesForCellWithIndexPath:path];
CGFloat progress = (float) path.item / (float) numberOfItems;
CGFloat theta = 2.0f * M_PI * progress;
// Update the scaling and rotation to match the current gesture
CGFloat scaledRadius = MIN(MAX(scale, 0.5f), 1.3f) * radius;
CGFloat rotatedTheta = theta + rotation + currentRotation;
// Calculate the new positions
CGFloat xPosition =
centerPoint.x + scaledRadius * cos(rotatedTheta);
CGFloat yPosition =
centerPoint.y + scaledRadius * sin(rotatedTheta);
attributes.size = [self itemSize];
attributes.center = CGPointMake(xPosition, yPosition);
return attributes;
}
@end
To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-6-Cookbook and go to the folder for Chapter 11.
The default flow layout wraps its rows to fit into a scrolling view that moves in just one direction. If you’re willing to do the math—and I warn you there’s quite a bit of it, and I’m not entirely convinced that I’ve gotten it all exactly right—you can create a custom layout subclass that scrolls in both directions and doesn’t wrap its lines. Figure 11-9 shows this layout.
Recipe 11-8 fully customizes its layout subclass, overriding collectionViewContentSize
and layoutAttributesForItemAtIndexPath:
to manually place each item. This implementation fully respects all spacing requests and delegate callbacks. In contrast, the normal flow layout attempts to fit items in while meeting various minimum values. This layout uses those values exactly, instead, adjusting the underlying scrolling view’s content size to precisely match sizing needs.
This recipe works by exhaustively calculating each layout element. What it doesn’t use, however, is the line-spacing property that describes how to wrap rows. This grid presentation never wraps any rows, so it ignores that entirely.
It also adds a new custom layout property, alignment
. This property controls whether each grid row aligns at the top, center, or bottom. It accomplishes this by looking at the overall height for an entire row, and then optionally offsetting items that are smaller than that height.
I’ve included the entire layout code to give you a sense of how much effort is involved for a complete custom subclass. The trick is, of course, in the details. Test layouts as thoroughly as possible over a wide range of source objects.
@implementation GridLayout
#pragma mark Items
// Does a delegate provide individual sizing?
- (BOOL) usesIndividualSizing
{
return [self.collectionView.delegate respondsToSelector:
@selector(collectionView:layout:sizeForItemAtIndexPath:)];
}
// Return cell size for an item
- (CGSize) sizeForItemAtIndexPath: (NSIndexPath *) indexPath
{
BOOL individuallySized = [self usesIndividualSizing];
CGSize itemSize = self.itemSize;
if (individuallySized)
itemSize = [(id <UICollectionViewDelegateFlowLayout>)
self.collectionView.delegate
collectionView:self.collectionView
layout:self sizeForItemAtIndexPath:indexPath];
return itemSize;
}
#pragma mark Insets
// Individual insets?
- (BOOL) usesIndividualInsets
{
return [self.collectionView.delegate respondsToSelector:
@selector(collectionView:layout:insetForSectionAtIndex:)];
}
// Return insets for section
- (UIEdgeInsets) insetsForSection: (NSInteger) section
{
UIEdgeInsets insets = self.sectionInset;
if ([self usesIndividualInsets])
insets = [(id <UICollectionViewDelegateFlowLayout>)
self.collectionView.delegate
collectionView:self.collectionView
layout:self insetForSectionAtIndex:section];
return insets;
}
#pragma mark Item Spacing
// Individual item spacing?
- (BOOL) usesIndividualItemSpacing
{
return [self.collectionView.delegate respondsToSelector:
@selector(layout:minimumInteritemSpacingForSectionAtIndex:)];
}
// Return spacing for section
- (CGFloat) itemSpacingForSection: (NSInteger) section
{
CGFloat spacing = self.minimumInteritemSpacing;
if ([self usesIndividualItemSpacing])
spacing = [(id <UICollectionViewDelegateFlowLayout>)
self.collectionView.delegate
collectionView:self.collectionView
layout:self
minimumInteritemSpacingForSectionAtIndex:section];
return spacing;
}
#pragma mark Layout Geometry
// Find the tallest subview
- (CGFloat) maxItemHeightForSection: (NSInteger) section
{
CGFloat maxHeight = 0.0f;
NSInteger numberOfItems =
[self.collectionView numberOfItemsInSection:section];
for (int i = 0; i < numberOfItems; i++)
{
NSIndexPath *indexPath = INDEXPATH(section, i);
CGSize itemSize = [self sizeForItemAtIndexPath:indexPath];
maxHeight = MAX(maxHeight, itemSize.height);
}
return maxHeight;
}
// "Horizontal" row-based extent from the start of the section to its end
- (CGFloat) fullWidthForSection: (NSInteger) section
{
UIEdgeInsets insets = [self insetsForSection:section];
CGFloat horizontalInsetExtent = insets.left + insets.right;
CGFloat collectiveWidth = horizontalInsetExtent;
NSInteger numberOfItems =
[self.collectionView numberOfItemsInSection:section];
for (int i = 0; i < numberOfItems; i++)
{
NSIndexPath *indexPath = INDEXPATH(section, i);
CGSize itemSize = [self sizeForItemAtIndexPath:indexPath];
collectiveWidth += itemSize.width;
collectiveWidth += [self itemSpacingForSection:section];
}
// Take back one spacer, n-1 fence post
collectiveWidth -= [self itemSpacingForSection:section];
return collectiveWidth;
}
// Bounding size for each section
- (CGSize) fullSizeForSection: (NSInteger) section
{
CGFloat headerExtent = (self.scrollDirection ==
UICollectionViewScrollDirectionHorizontal) ?
self.headerReferenceSize.width :
self.headerReferenceSize.height;
CGFloat footerExtent =(self.scrollDirection ==
UICollectionViewScrollDirectionHorizontal) ?
self.footerReferenceSize.width :
self.footerReferenceSize.height;
UIEdgeInsets insets = [self insetsForSection:section];
CGFloat verticalInsetExtent = insets.top + insets.bottom;
CGFloat maxHeight = [self maxItemHeightForSection:section];
CGFloat fullHeight = headerExtent + footerExtent +
verticalInsetExtent + maxHeight;
CGFloat fullWidth = [self fullWidthForSection:section];
return CGSizeMake(fullWidth, fullHeight);
}
// How far is each item offset within the section
- (CGFloat) horizontalInsetForItemAtIndexPath: (NSIndexPath *) indexPath
{
UIEdgeInsets insets = [self insetsForSection:indexPath.section];
float horizontalOffset = insets.left;
if (indexPath.item > 0)
{
for (int i = 0; i < indexPath.item; i++)
{
CGSize itemSize = [self sizeForItemAtIndexPath:
INDEXPATH(indexPath.section, i)];
horizontalOffset += (itemSize.width +
[self itemSpacingForSection:indexPath.section]);
}
}
return horizontalOffset;
}
// How far is each item down
- (CGFloat) verticalInsetForItemAtIndexPath: (NSIndexPath *) indexPath
{
CGSize thisItemSize = [self sizeForItemAtIndexPath:indexPath];
CGFloat verticalOffset = 0.0f;
// Previous sections
if (indexPath.section > 0)
{
for (int i = 0; i < indexPath.section; i++)
verticalOffset += [self fullSizeForSection:i].height;
}
// Header
CGFloat headerExtent = (self.scrollDirection ==
UICollectionViewScrollDirectionHorizontal) ?
self.headerReferenceSize.width : self.headerReferenceSize.height;
verticalOffset += headerExtent;
// Top inset
UIEdgeInsets insets = [self insetsForSection:indexPath.section];
verticalOffset += insets.top;
// Vertical centering
CGFloat maxHeight = [self maxItemHeightForSection:indexPath.section];
CGFloat fullHeight = (maxHeight - thisItemSize.height);
CGFloat midHeight = fullHeight / 2.0f;
switch (self.alignment)
{
case GridRowAlignmentNone:
case GridRowAlignmentTop:
break;
case GridRowAlignmentCenter:
verticalOffset += midHeight;
break;
case GridRowAlignmentBottom:
verticalOffset += fullHeight;
break;
default:
break;
}
return verticalOffset;
}
#pragma mark Layout Attributes
// Provide per-item placement
- (UICollectionViewLayoutAttributes *) layoutAttributesForItemAtIndexPath:
(NSIndexPath *) indexPath
{
UICollectionViewLayoutAttributes *attributes =
[UICollectionViewLayoutAttributes
layoutAttributesForCellWithIndexPath:indexPath];
CGSize thisItemSize = [self sizeForItemAtIndexPath:indexPath];
float verticalOffset =
[self verticalInsetForItemAtIndexPath:indexPath];
float horizontalOffset =
[self horizontalInsetForItemAtIndexPath:indexPath];
if (self.scrollDirection == UICollectionViewScrollDirectionVertical)
attributes.frame = CGRectMake(horizontalOffset,
verticalOffset, thisItemSize.width, thisItemSize.height);
else
attributes.frame = CGRectMake(verticalOffset,
horizontalOffset, thisItemSize.width, thisItemSize.height);
return attributes;
}
// Return full extent
- (CGSize) collectionViewContentSize
{
NSInteger sections = self.collectionView.numberOfSections;
CGFloat maxWidth = 0.0f;
CGFloat collectiveHeight = 0.0f;
for (int i = 0; i < sections; i++)
{
CGSize sectionSize = [self fullSizeForSection:i];
collectiveHeight += sectionSize.height;
maxWidth = MAX(maxWidth, sectionSize.width);
}
if (self.scrollDirection == UICollectionViewScrollDirectionVertical)
return CGSizeMake(maxWidth, collectiveHeight);
else
return CGSizeMake(collectiveHeight, maxWidth);
}
// Provide grid layout attributes
- (NSArray *) layoutAttributesForElementsInRect: (CGRect) rect
{
NSMutableArray *attributes = [NSMutableArray array];
for (NSInteger section = 0;
section < self.collectionView.numberOfSections; section++)
for (NSInteger item = 0 ;
item < [self.collectionView numberOfItemsInSection: section];
item++)
{
UICollectionViewLayoutAttributes *layout =
[self layoutAttributesForItemAtIndexPath:
INDEXPATH(section, item)];
[attributes addObject:layout];
}
return attributes;
}
- (BOOL) shouldInvalidateLayoutForBoundsChange: (CGRect) oldBounds
{
return YES;
}
@end
To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-6-Cookbook and go to the folder for Chapter 11.
I’m quite sure there’s a better way to handle nonstandard collection view menus, like the one shown in Figure 11-10. Alas, my attempts to make them happen using the delegate method collectionView:shouldShowMenuForItemAtIndexPath:
failed to produce any meaningful results. Instead, I created a custom cell class and added a double-tap gesture recognizer.
When activated, the callback sets the cell as the first responder (usually with a complaint about not knowing the type of the collection view’s first responder) and presents a standard menu.
Recipe 11-9 shows the relevant details. The cell subclass declares that it can become first responder, a necessary precondition for presenting menus. It sets the menu items it wants to work with and then adds the canPerformAction:withSender:
support that confirms each item’s appearance. Figure 11-10 displays the menu created by this code.
- (BOOL) canBecomeFirstResponder
{
return YES;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (action == @selector(ghostSelf)) return YES;
if (action == @selector(popSelf)) return YES;
if (action == @selector(rotateSelf)) return YES;
if (action == @selector(colorize)) return YES;
return NO;
}
- (void) tapped: (UIGestureRecognizer *) uigr
{
if (uigr.state != UIGestureRecognizerStateRecognized) return;
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
[self becomeFirstResponder];
UIMenuController *menu = [UIMenuController sharedMenuController];
UIMenuItem *pop = [[UIMenuItem alloc]
initWithTitle:@"Pop" action:@selector(popSelf)];
UIMenuItem *rotate = [[UIMenuItem alloc]
initWithTitle:@"Rotate" action:@selector(rotateSelf)];
UIMenuItem *ghost = [[UIMenuItem alloc]
initWithTitle:@"Ghost" action:@selector(ghostSelf)];
UIMenuItem *colorize = [[UIMenuItem alloc]
initWithTitle:@"Colorize" action:@selector(colorize)];
[menu setMenuItems:@[pop, rotate, ghost, colorize]];
[menu update];
[menu setTargetRect:self.bounds inView:self];
[menu setMenuVisible:YES animated:YES];
}
To find this recipe’s full sample project, point your browser to https://github.com/erica/iOS-6-Cookbook and go to the folder for Chapter 11.
This chapter introduced collection views, along with their custom layout flows. You read how to create both basic collection view controllers as well as their stand-alone views. You discovered how to set critical layout properties. You learned about creating live effect feedback and insertion and deletion dynamic effects. Before moving on to the next chapter, here are a few points to consider about collection views:
• Collection views offer an amazing amount of power without requiring a lot of coding. Most things that were maddening and nearly impossible with table views are now possible with a much more powerful set of APIs.
• This chapter barely touched on header and footer views, and didn’t use decoration views at all. See the sample code included with this chapter for more details on the fine points of creating custom supplementary view classes.
• Transform-based updates help bring life to your collection view layouts. Don’t be afraid to let your interfaces animate to respond to user interactions. At the same time, avoid adding effects simply for the sake of adding effects. A little animation goes a long way.
• Speaking of animations, the same inserted and deleted attribute methods this chapter used for items are available for supplementary elements. This feature lets you animate the arrival and departure of new sections in your collection.
• On a similar note, integrate gestures meaningfully. If a user isn’t likely to discover your long-press or triple-tap add or deletion request, skip it. Instead, use pop-ups, menus, floating overlays, or simple buttons to communicate how items can be managed and changed.
• When exploring layout, don’t depend on the flow layout documentation. Look instead through the UICollectionViewLayout
abstract parent class. It details all the core methods you override.
• Finally, always test on devices. Layouts, especially ones that update frequently or use transforms, can tax the simulator. Device testing, along with Instruments, will better reflect whether you’re actually asking too much from your presentation.
13.58.113.193