Creating Custom Layouts

Before you start building custom layouts, consider whether doing so is really necessary. The UICollectionViewFlowLayout class provides a significant amount of behavior that has already been optimized for efficiency and that can be adapted in several ways to achieve many different types of standard layouts. The only times to consider implementing a custom layout are in the following situations:

The good news is that, from an API perspective, implementing a custom layout is not difficult. The hardest part is performing the calculations needed to determine the positions of items in the layout. When you know the locations of those items, providing that information to the collection view is straightforward.

Subclassing UICollectionViewLayout

For custom layouts, you want to subclass UICollectionViewLayout, which provides you with a fresh starting point for your design. Only a handful of methods provide the core behavior for your layout object and are required in your implementation. The rest of the methods are there for you to override as needed to tweak the layout behavior. The core methods handle the following crucial tasks:

Although you can create a functional layout object that implements just the core methods, your layout is likely to be more engaging if you implement several of the optional methods as well.

The layout object uses information provided by its data source to create the collection view’s layout. Your layout communicates with the data source by calling methods on the collectionView propery, which is accessible in all of the layout’s methods. Keep in mind what your collection view knows and doesn’t know during the layout process. Because the layout process is under way, the collection view cannot track the layout or positioning of views. So even though the layout object will not restrict you from calling any of the collection view’s methods, refrain from relying on the collection view for anything other than the data necessary to compute your layout.

Understanding the Core Layout Process

The collection view works directly with your custom layout object to manage the overall layout process. When the collection view determines that it needs layout information, it asks your layout object to provide it. For example, the collection view asks for layout information when it is first displayed or is resized. You can also tell the collection view to update its layout explicitly by calling the invalidateLayout method of the layout object. That method throws away the existing layout information and forces the layout object to generate new layout information.

During the layout process, the collection view calls specific methods of your layout object. These methods are your chance to calculate the position of items and to provide the collection view with the primary information it needs. Other methods may be called, too, but these methods are always called during the layout process in the following order:

  1. Use the prepareLayout method to perform the up-front calculations needed to provide layout information.

  2. Use the collectionViewContentSize method to return the overall size of the entire content area based on your initial calculations.

  3. Use the layoutAttributesForElementsInRect: method to return the attributes for cells and views that are in the specified rectangle.

Figure 5-1 illustrates how you can use the preceding methods to generate your layout information.

Figure 5-1  Laying out your custom content

The prepareLayout method is your opportunity to perform whatever calculations are needed to determine the position of the cells and views in the layout. At a minimum, you should compute enough information in this method to be able to return the overall size of the content area, which you return to the collection view in step 2.

The collection view uses the content size to configure its scroll view appropriately. For instance, if your computed content size expands past the bounds of the current device’s screen both vertically and horizontally, the scroll view adjusts to allow scrolling in both directions simultaneously. Unlike the UICollectionViewFlowLayout, it does not by default adjust the layout of content to scroll in only one direction.

Based on the current scroll position, the collection view then calls your layoutAttributesForElementsInRect: method to ask for the attributes of the cells and views in a specific rectangle, which may or may not be the same as the visible rectangle. After returning that information, the core layout process is effectively complete.

After layout finishes, the attributes of your cells and views remain the same until you or the collection view invalidates the layout. Calling the invalidateLayout method of your layout object causes the layout process to begin again, starting with a new call to the prepareLayout method. The collection view can also invalidate your layout automatically during scrolling. If the user scrolls its content, the collection view calls the layout object’s shouldInvalidateLayoutForBoundsChange: method and invalidates the layout if that method returns YES.

Creating Layout Attributes

The attributes objects that your layout is responsible for are instances of the UICollectionViewLayoutAttributes class. These instances can be created in a variety of different methods in your app. When your app is not dealing with thousands of items, it makes sense to create these instances while preparing the layout, because the layout information can be cached and referenced rather than computed on the fly. If the costs of computing all the attributes up front outweighs the benefits of caching in your app, it is just as easy to create attributes in the moment when they are requested.

Regardless, when creating new instances of the UICollectionViewLayoutAttributes class, use one of the following class methods:

You must use the correct class method based on the type of the view being displayed because the collection view uses that information to request the appropriate type of view from the data source object. Using the incorrect method causes the collection view to create the wrong views in the wrong places and your layout does not appear as intended.

After creating each attributes object, set the relevant attributes for the corresponding view. At a minimum, set the size and position of the view in the layout. In cases where the views of your layout overlap, assign a value to the zIndex property to ensure a consistent ordering of the overlapping views. Other properties let you control the visibility or appearance of the cell or view and can be changed as needed. If the standard attributes class does not suit your app’s needs, you can subclass and expand it to store other information about each view. When subclassing layout attributes, it’s required that you implement the isEqual: method for comparing your custom attributes because the collection view uses this method for some of its operations.

For more information about layout attributes, see UICollectionViewLayoutAttributes Class Reference.

Preparing the Layout

At the beginning of the layout cycle, the layout object calls prepareLayout before beginning the layout process. This method is your chance to calculate information that later informs your layout. The prepareLayout method is not required to implement a custom layout but is provided as an opportunity to make initial calculations if necessary. After this method is called, your layout must have enough information to calculate the collection view’s content size, the next step in the layout process. The information, however, can range from this minimum requirement to creating and storing all the layout attributes objects your layout will use. Use of the prepareLayout method is subject to the infrastructure of your app and to what makes sense to compute up front versus what to compute upon request. For an example of what the prepareLayout method might look like, see “Preparing the Layout.”

Providing Layout Attributes for Items in a Given Rectangle

During the final step of the layout process, the collection view calls your layout object’s layoutAttributesForElementsInRect: method. The purpose of this method is to provide layout attributes for every cell and every supplementary or decoration view that intersects the specified rectangle. For a large scrollable content area, the collection view may just ask for the attributes of items in the portion of that content area that is currently visible. In Figure 5-2, the currently visible content that your layout object needs to create attribute objects for is cells 6 through 20 along with the second header view. You must be prepared to provide layout attributes for any portion of your collection view content area. Such attributes might be used to facilitate animations for inserted or deleted items.

Figure 5-2  Laying out only the visible views

Because the layoutAttributesForElementsInRect: method is called after your layout object’s prepareLayout method, you should already have most of the information you need in order to return or create the required attributes. The implementation of your layoutAttributesForElementsInRect: method follows these steps:

  1. Iterate over the data generated by the prepareLayout method to either access cached attributes or create new ones.

  2. Check the frame of each item to see whether it intersects the rectangle passed to the layoutAttributesForElementsInRect: method.

  3. For each intersecting item, add a corresponding UICollectionViewLayoutAttributes object to an array.

  4. Return the array of layout attributes to the collection view.

Depending on how you manage your layout information, you might create UICollectionViewLayoutAttributes objects in your prepareLayout method or wait and do it in your layoutAttributesForElementsInRect: method. While forming an implementation that matches the needs of your application, keep in mind the benefits of caching layout information. Computing new layout attributes repeatedly for cells is an expensive operation, one that can have noticeably detrimental effects on your app’s performance. That said, when the amount of items your collection view manages is large, it may make more sense (for performance) to create the layout attributes when requested. It’s simply a matter of figuring out which strategy makes most sense for your app.

For a specific example of how one might implement layoutAttributesForElementsInRect:, see “Providing Layout Attributes.”

Providing Layout Attributes On Demand

The collection view periodically asks your layout object to provide attributes for individual items outside of the formal layout process. For example, the collection view asks for this information when configuring insertion and deletion animations for an item. Your layout object must be prepared to provide the layout attributes for each cell, supplementary view, and decoration view it supports. You do this by overriding the following methods:

Your implementation of these methods should retrieve the current layout attributes for the given cell or view. Every custom layout object is expected to implement the layoutAttributesForItemAtIndexPath: method. If your layout does not contain any supplementary views, you do not need to override the layoutAttributesForSupplementaryViewOfKind:atIndexPath: method. Similarly, if it does not contain decoration views, you do not need to override the layoutAttributesForDecorationViewOfKind:atIndexPath: method. When returning attributes, you should not update the layout attributes. If you need to change the layout information, invalidate the layout object and let it update that data during a subsequent layout cycle.

Connecting Your Custom Layout for Use

There are two ways to link your custom layout to the collection view: programmatically or through storyboards. The collection view links to its layout through a writable property, collectionViewLayout. To set the layout to your custom implementation, set your collection view’s layout property to an instance of your custom layout object. Listing 5-1 shows the line of code needed.

Listing 5-1  Linking your custom layout

self.collectionView.collectionViewLayout = [[MyCustomLayout alloc] init];

Otherwise, from your storyboard, open the Document Outline panel and select your collection view (it is listed in the drop-down menu for your controller). With the collection view selected, open the Attributes inspector in the Utilities pane, and underneath the section labeled Collection View change the Layout option from Flow to Custom. The option beneath it changes from Scroll Direction to Class, and you can now select your custom layout class.

Making Your Custom Layouts More Engaging

Providing layout attributes for each cell and view during the layout process is required, but there are other behaviors that can improve the user experience with your custom layout. Implementing these behaviors is optional but recommended.

Elevating Content Through Supplementary Views

Supplementary views are separate from the collection view’s cells and have their own set of layout attributes. Like cells, these views are provided by the data source object, but their purpose is to enhance the main content of your app. For example, the UICollectionViewFlowLayout uses supplementary views for section headers and footers. Another app could use supplementary views to give each cell its own text label to display information about that cell. Like collection view cells, supplementary views undergo a recycling process to optimize the amount of resources used by the collection view. Therefore, all supplementary views used in your app should be subclassed from the UICollectionReusableView class.

The steps for adding supplementary views to your layouts are as follows:

  1. Register your supplementary view to the collection view’s layout object using either the registerClass:forSupplementaryViewOfKind:withReuseIdentifier: or registerNib:forSupplementaryViewOfKind:withReuseIdentifier: method.

  2. In your data source, implement collectionView:viewForSupplementaryElementOfKind:atIndexPath:. Because these views are reusable, call dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath: to dequeue, or create, a new reusable view and set any necessary data before returning it.

  3. Create layout attributes objects for your supplementary views just as you do for cells.

  4. Include these layout attributes objects in the array of attributes returned by the layoutAttributesForElementsInRect: method.

  5. Implement the layoutAttributesForSupplementaryViewOfKind:atIndexPath: method to return the attributes object for the specified supplementary view whenever queried.

The process for creating the attributes objects for supplementary views in your custom layout is nearly identical to the process for cells, but differs in that a custom layout can have multiple types of supplementary views but is restricted to one type of cell. This is because supplementary views are meant to enhance the main content and are therefore separate from it. There are many ways in which an app’s content can be supplemented, and so each of the supplementary view’s methods specifies which kind of view is being addressed to distinguish it from the others and allow your layout to compute its attributes correctly based on its type. When registering a supplementary view for use, the string you provide is used by the layout object to distinguish the view from others. For an example of incorporating supplementary views into your custom layout, see “Incorporating Supplementary Views.”

Including Decoration Views in Your Custom Layouts

Decoration views are visual adornments that enhance the appearance of your collection view layouts. Unlike cells and supplementary views, decoration views provide visual content only and are thus independent of the data source. You can use them to provide custom backgrounds, fill in the spaces around cells, or even obscure cells if you want. Decoration views are defined and managed solely by the layout object and do not interact with the collection view’s data source object.

To add decoration views to your layouts, do the following:

  1. Register your decoration view with the layout object using the registerClass:forDecorationViewOfKind: or registerNib:forDecorationViewOfKind: method. Although this approach is similar to registering cells and supplementary views, remember that registering decoration views occurs within the layout object, not within the data source.

  2. In your layout object’s layoutAttributesForElementsInRect: method, create attributes for your decoration views just as you do for your cells and supplementary views.

  3. Implement the layoutAttributesForDecorationViewOfKind:atIndexPath: method in your layout object and return the attributes for your decoration views when asked.

  4. Optionally, implement the initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath: and finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath: methods to handle animations for the appearance and disappearance of your decoration views. For more information, see “Making Insertion and Deletion Animations More Interesting”

The creation process for decoration views is different from the process for cells and supplementary views. Registering a class or nib file is all you have to do to ensure that decoration views are created when they are needed. Because they are purely visual, decoration views are not expected to need any configuration beyond what is already done in the provided nib file or by the object’s initWithFrame: method. For this reason, when a decoration view is needed, the collection view creates it for you and applies the attributes provided by the layout object. Any decoration views should still be a subclass of UICollectionReusableView because the layout object employs a recycling mechanism for its decoration views.

Making Insertion and Deletion Animations More Interesting

Inserting and deleting cells and views poses an interesting challenge during layout. Inserting a cell can cause the layout for other cells and views to change. Even though the layout object knows how to animate existing cells and views from their current locations to new locations, it has no current location for the cell being inserted. Rather than insert the new cell without animations, the collection view asks the layout object to provide a set of initial attributes to use for the animation. Similarly, when a cell is deleted, the collection view asks the layout object to provide a set of final attributes to use for the endpoint of any animations.

To understand how initial attributes work, it helps to see an example. The starting layout (Figure 5-3) shows a collection view that initially contains only three cells. When a new cell is inserted, the collection view asks the layout object to provide initial attributes for the cell being inserted. In this case, the layout object sets the initial position of the cell to the middle of the collection view and sets its alpha value to 0 to hide it. During the animations, this new cell appears to fade in and move from the center of the collection view to its final position in the lower-right corner.

Figure 5-3  Specifying the initial attributes for an item appearing onscreen

Listing 5-2 shows the code you might use to specify the initial attributes for the inserted cell from Figure 5-3. This method sets the position of the cell to the center of the collection view and makes it transparent. The layout object would then provide the final position and alpha for the cell as part of the normal layout process.

Listing 5-2  Specifying the initial attributes for an inserted cell

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
   UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
   attributes.alpha = 0.0;
 
   CGSize size = [self collectionView].frame.size;
   attributes.center = CGPointMake(size.width / 2.0, size.height / 2.0);
   return attributes;
}

The process for handling deletions is identical to the process for insertions except that you specify the final attributes instead of the initial attributes. From the previous example, if you used the same attributes that you used when inserting the cell, deleting the cell would cause it to fade out while moving to the center of the collection view. There are six methods available to you within the UICollectionViewLayout class—two separate methods (for initial and final attributes) for items, supplementary views, and decoration views.

Improving the Scrolling Experience of Your Layout

Your custom layout object can influence the scrolling behavior of the collection view to create a better user experience. When scrolling-related touch events end, the scroll view determines the final resting place of the scrolling content based on the current speed and deceleration rate in effect. When the collection view knows that location, it asks its layout object if the location should be modified by calling its targetContentOffsetForProposedContentOffset:withScrollingVelocity: method. Because it calls this method while the underlying content is still moving, your custom layout can affect the final resting point of the scrolling content.

Figure 5-4 demonstrates how you might use your layout object to change the scrolling behavior of the collection view. Suppose the collection view offset starts at (0, 0) and the user swipes left. The collection view computes where the scrolling would naturally stop and provides that value as the “proposed” content offset value. Your layout object might change the proposed value to ensure that when scrolling stops, an item is centered precisely in the visible bounds of the collection view. This new value becomes the target content offset and is what you return from your targetContentOffsetForProposedContentOffset:withScrollingVelocity: method.

Figure 5-4  Changing the proposed content offset to a more appropriate value

Tips for Implementing Your Custom Layouts

Here are some tips and suggestions for implementing your custom layout objects: