Displaying Content Using UICollectionView - iOS UICollectionView: The Complete Guide, Second Edition (2014)

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

Chapter 2. Displaying Content Using UICollectionView

Now that you understand how collection views fit within an iOS app using the Model-View-Control (MVC) paradigm, it’s time to get to the good stuff: code. This chapter starts off easy and shows how you can use storyboards or .xibs to set up collection views, and then it shows you how to set them up in code. Collection views extend their UIScrollView superclass, so the chapter takes a brief detour to show how to use that to your advantage with UIScrollViewDelegate. You begin customizing actual content to show to your users using cell reuse before finishing off with a case study on performance.

Setting Up Using Code and Storyboards

Traditionally, .xib files were used to lay out interface code for OS X and iOS apps. These files are “freeze-dried” versions of your interface that are thawed at runtime. The benefit of .xibs is that they’re easy to use to create basic interfaces; you usually have one instance ofUIViewController per .xib.

Storyboards, first introduced in iOS 5 in 2011, enable developers to visually lay out the interaction between view controllers. Not only can developers visualize the connections between view controllers, but they can also define how their entire application transitions from one view controller to another. The key thing about storyboards is their efficiency; a huge .xib file, which has to be completely loaded into memory, can delay the time it takes for your app to launch. Storyboards efficiently lazy-load only the view controllers necessary.

Of course, anything you can do in a .xib file or storyboard can be done using cold, hard code. If you are integrating collection views into your existing application, which uses .xib files or storyboards, it might be convenient to continue to use them. However, because collection views requirethe use of code for layout, it’s often easier to avoid using .xibs and storyboards altogether. Nevertheless, this chapter explains how to set up the collection view from the last chapter using a storyboard and then set it up again using only code.

Create a new Xcode project with the Single View template. Make sure that Use Storyboards is checked. Open the MainStorboard.storyboard file and delete the view controller that’s already there. Drag a Collection view controller from the object library in the right pane onto the empty canvas, as shown in Figure 2.1.

Image

Figure 2.1 Basic collection view using storyboards

You could run the app right now and it would work, but it would be pretty boring. The storyboard has set up the delegate and data source outlets of the collection view to point to your collection view controller. The next step is to customize what that view controller actually does. This part is easy, because you’re just going to copy the existing code from Chapter 1, “Understanding Model-View-Controller on iOS.”

Open the header for your view controller and change which class it inherits from (by changing UIViewController to UICollectionViewController). Then copy the implementation file in its entirety from the last chapter. The last, important step is to tell your storyboard which view controller it should use. Click the Collection view controller in the storyboard and open the Identity Inspector. Where it says Class, you see the default placeholder of UICollectionViewController. Boring! Replace that with the name of your view controller—in my case, it’sAFViewController.

This step is crucial; it’s how the storyboard knows what code to execute when laying out the collection view. Run your app, and you see the same output as from Chapter 1.

Using storyboards or .xibs, you have an opportunity to change the visual display of the collection view without any code. Select the collection view in the storyboard and open the Attributes Inspector. Here, you can change the scroll direction of the collection view from Vertical, the default, to Horizontal. You can also change properties of the collection view that belong to its superclass, UIScrollView. Change the Style to white, which makes the scroll indicator visible against the black background.

Open the Size Inspector, and you can change the attributes of the collection view layout, shown in Figure 2.2. (Collection views abstract these properties to their layout objects; read more on that in Chapter 3, “Contextualizing Content.”) Here, you can change the cell size, which is 50 by 50 points by default. Bump the width down to 20 and keep the height set to 50. The header and footer sizes don’t work just yet because you haven’t used headers or footers.

Image

Figure 2.2 Size Inspector of a collection view layout

You can change the distance between cells in the collection view using the Min Spacing section in the Size Inspector. This is only the minimum distance; the default layout, called Flow, makes sure that cells are a minimum distance from one another. The Section Insets area of the Size Inspector enables you to specify the distance surrounding an entire section. (Remember that you only have one section so far.) You take a closer look at section insets in Chapter 3, so don’t worry about the specifics for now. It’s a personal pet peeve of mine to have too small a margin around content, so bump up the section insets to 10 points each. Run the app to see the visual differences in the collection view. It should resemble Figure 2.3.

Image

Figure 2.3 Changes made with storyboards

Not bad at all. Don’t worry that the status bar is visible in front of our content; that is the default on iOS 7. We’ll solve this problem later by placing our Collection view controller inside of a navigation controller. The problem with Figure 2.3 is that only some of the properties of a collection view layout are accessible with storyboards or .xib files. In addition, if you override the properties you’ve set in a storyboard in code, or you forget that you’ve set something in the storyboard, it can lead to a debugging headache. For this reason, I strongly prefer to use a code-only approach with collection views.

Now you can re-create your interface using only code. Create a new Xcode project with the Empty Application template. (For anyone who has never created an app from an empty template, this can be a big step.) Create a new file using File, New, File or ImageN. Select Objective-C Class and call it something like AFViewController. In the field for Subclass, enter UICollectionViewController. Make sure not to select With XIB for User Interface.

Open the application delegate implementation file and add an #import statement to import the new view controller’s header file. Change the implementation to look like the code in Listing 2.1.

Listing 2.1 Setting Up the Application


#import "AFAppDelegate.h"
#import "AFViewController.h"

@implementation AFAppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];

UICollectionViewFlowLayout *collectionViewLayout =
[[UICollectionViewFlowLayout alloc] init];

collectionViewLayout.scrollDirection =
UICollectionViewScrollDirectionHorizontal;
collectionViewLayout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
collectionViewLayout.itemSize = CGSizeMake(20, 50);
self.window.rootViewController = [[AFViewController alloc]
initWithCollectionViewLayout:collectionViewLayout];

self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];

return YES;
}


Next, open the view controller’s implementation file and add the following line to the viewDidLoad method (see Listing 2.2).

Listing 2.2 Setting the Scroll Indicator Color


-(void)viewDidLoad
{
[super viewDidLoad];

//All that other stuff

self.collectionView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
}


Build and run the app, and you see that everything you customized using storyboards has been replicated using just code. High five!

Before you dive deeper into collection views and laying out content, the following section takes you on a quick diversion to discuss UIScrollView.

UIScrollView: A Brief Overview

UICollectionView is a direct subclass of UIScrollView, much like UITableView.

Similarly to the UICollectionView inheritance, the UICollectionViewDelegate protocol conforms to the UIScrollViewDelegate protocol. In practical terms, this means that if an object is the delegate of a collection view, it receives callbacks notifying it ofUICollectionViewDelegate events as well as UIScrollViewDelegate events.

UIScrollView is a versatile class in UIKit and has been around since iOS was iPhone OS 2.0. It provides a friendly way for developers to scroll content, whether it be a list of emails, a grid of apps, or a single photo. If you can scroll something in any given app, chances are that the app uses a scroll view.

Scroll views give a familiar feel to the user and make any application that uses them seem more like it belongs in iOS and less like its developer wrote his own scroll view. Scroll views offer a lot of power to developers for very little work; all that developers need to do is set up the scroll view and add subviews to it. In addition, you get to rely on the work that Apple has already done for you, like emulating physics and deceleration. Take a look at an example in which the user can scroll to see more content than can fit on the screen simultaneously.

Create a new Xcode project with the Single View template. Copy a large image into the project and open the main view controller’s implementation file. Replace the viewDidLoad implementation with the one in Listing 2.3.

Listing 2.3 A Simple Scroll View Example


-(void)viewDidLoad
{
[super viewDidLoad];

//First we create an image to display to the user.
//Replace "cat.jpg" with whatever your image is named
UIImage *image = [UIImage imageNamed:@"cat.jpg"];

//Next we create an image view to display the image.
//It should be the same size as the image with its origin
//in the top-left corner
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.frame = CGRectMake(0, 0,
image.size.width, image.size.height);

//Finally we create our scroll view. We give it a frame
//corresponding to our view's bounds so it fills the entire view
UIScrollView *scrollView = [[UIScrollView alloc]
initWithFrame:self.view.bounds];

//This line is very important - it makes the scroll view scroll
scrollView.contentSize = image.size;
//This is just to get rotation to work correctly
scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

//Finally, set up the view hierarchy
[scrollView addSubview:imageView];
[self.view addSubview:scrollView];
}


Run the application, and you see output similar to Figure 2.4; the image is too large to fit on the screen at one time, but the user can scroll around the image to see it all. (Notice the scroll indicators.) The magic that makes this all work is the contentSize property. This is a CGSize value that represents the size (in points) of the scrollable area. Its default value is zero, and it must be set to use any scroll view, even if the content size is smaller than the scroll view’s own size.

Image

Figure 2.4 A simple scroll view example

When the scroll view knows the size of the content it’s displaying, it scrolls. The contentSize property can change at any time.

Figure 2.5 demonstrates the idea of content size. The light region of the photo, in the upper left, defines the visible part of the image when the application first launches. This is the size of the scroll view and is represented by dashed lines. The solid lines represent the content size of the scroll view.

Image

Figure 2.5 Content size example

When the user scrolls the scroll view, the content area visible to the user changes. The position of the content view within the scroll view is called the content offset and is represented by the contentOffset property, a CGPoint value. This property is defined by the distance from the visible region’s origin (top-left corner) to the origin of the content. Figure 2.6 demonstrates content offset with white dashed lines. The content size remains the same, but the content offset changes to respond to user interaction.

Image

Figure 2.6 Content offset example

Content offset can be changed programmatically; the contentOffset property is readwrite. More interestingly, you can use the setContentOffset:animated: method to animate the change in content offset. This “moves” the scroll view, just as it would if the user moved it herself. The content offset can also be changed with scrollRectToVisible:animated:, but this is more often used with zooming than simple scrolling.

The last thing I want to mention about scroll views is the contentInset property. This is a UIEdgeInset value that represents the area around the scroll view’s content that it should “pad.” Setting the contentInset property to UIEdgeInsetsMake(10, 10, 10, 10) would create a 10-point margin surrounding the scroll view’s content. The edge inset values can also be negative; this would represent area around the scroll view content that can’t be seen by the user (unless she scrolls past the edge of the scroll view). Try playing around with contentInset to see how it works.

This contentInset is a widely used property and is often employed using UITableView and custom pull-to-refresh controls. It’s also useful if you have a navigation bar over top of a view controller with wantsFullScreenLayout set to YES. The inset’s top value would be equal to negative the height of the status bar and the navigation bar.

Those are the three main components to UIScrollView: contentSize, contentOffset, and contentInset. Now it’s time for a quick discussion about the scroll view delegate before the chapter moves on to some more collection view material.

There are three groups of methods in UIScrollViewDelegate: those responding to dragging and scrolling, those responding to zooming, and those responding to scrolling animations initiated explicitly by code (see Table 2.1). You’re going to be dealing only with the first and last groups because collection views don’t use the zoom functionality of UIScrollView.

Image

Image

Table 2.1 Useful UIScrollViewDelegate Methods

You use some scroll view delegate methods later on in more advanced chapters in this book and in some case studies. They are useful tools to solving many problems, and you should be aware of them.

UICollectionViewCell Reuse: How and Why

UICollectionView uses a memory-efficient scheme to configure individual cells for display. As one software engineer at Apple phrased it, “malloc is expensive.”

What he meant was that allocating new portions of memory is actually an expensive operation if you do it a lot. What UICollectionView does is very clever: It reuses cells it’s no longer displaying.


Note

This should sound familiar to anyone familiar with UITableView. With iOS 6, Apple took the best parts of UITableView to make UICollectionView. Many things will seem familiar, but you might be surprised at how much is new.


UICollectionView relies on its dataSource to tell it how many cells to display and to configure each individual cell before it is presented to the user. When scrolling, this needs to be incredibly fast, which is why cells have reuse. The following explains exactly what happens.

For every type of cell that’s going to be displayed, you should use a cell reuse identifier. This is an NSString that you typically store as a static variable. Before any cell with that reuse identifier can be displayed, it needs to be registered with the collection view. This is a big departure fromUITableView. You usually register cells in viewDidLoad and don’t reregister them later on.

When registering a cell, you provide either a UINib instance or a Class. I prefer a class instead of a nib because it gives me more control over the layout and performance.

Use either the registerClass:forCellWithReuseIdentifier: or registerNib:forCellWithReuseIdentifier: to register cells. From that point on, whenever dequeueReusableCellWithReuseIdentifier:forIndexPath: is called, you are guaranteed to have an allocated, initialized cell corresponding to your reuse identifier (see Figure 2.7).

Image

Figure 2.7 Collection view cell reuse

This differs from UITableView, which historically required developers to check for a nil return value from an attempt to dequeue a cell (though it now supports the new method). With collection views, you are guaranteed to be returned a valid cell.

If your collection view only ever has 20 cells visible onscreen simultaneously, your collection view is only ever allocated 20 cells; when a cell scrolls offscreen, it’s added to a reuse queue to be reused again. This technique lets applications maintain an insanely low memory footprint and an insanely high frame rate while scrolling through a collection view with hundreds or thousands of cells.

Most examples in this book, and most of the real-world uses for collection views, only display one type of cell and therefore have just one reuse identifier. It’s completely reasonable to have more than one type of identifier if you’re displaying more than one type of cell.

Displaying Content to Users

Alright! You’ve made it through a chapter on MVC and half a chapter on the basics of UICollectionView. It’s high time to see some code.

You’re going to build a basic application that displays some custom content to the user. It’s going to be an iPad app, so you can use really big cells. What you’re going to do at first is build a basic collection view that enables the user to add new cells with a plus button, and the cells display the time that they were added. This is just a warm-up for what comes later.

Create a new Xcode project using the Empty Application template. Create a new file, an Objective-C class that extends UICollectionViewController, and give it a suitable name. In your application delegate’s implementation file, #import the view controller’s header and create an instance of the view controller to be the root view controller of a navigation controller, the window’s root view controller (see Listing 2.4).

Listing 2.4 Setting Up the Application


#import "AFAppDelegate.h"
@implementation AFAppDelegate

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen]
bounds]];
self.window.backgroundColor = [UIColor whiteColor];

UICollectionViewFlowLayout *flowLayout =
[[UICollectionViewFlowLayout alloc] init];
AFViewController *viewController =
[[AFViewController alloc] initWithCollectionViewLayout:flowLayout];

UINavigationController *navigationController =
[[UINavigationController alloc]
initWithRootViewController:viewController];
navigationController.navigationBar.barStyle = UIBarStyleBlack;
self.window.rootViewController = navigationController;

[self.window makeKeyAndVisible];
return YES;
}


You’re relying on a UINavigationController because it provides a lot of nice things for free. In this case, you get a cool navigation bar on which you can include buttons. This implementation of applicationDidFinishLaunchingWithOptions: is a little more lightweight than the example earlier in this chapter; you’re going to be following some “best practices” a little closer this time. The app delegate creates just the basics for the view controller, and it further customizes itself.

Create a new Objective-C class that extends UICollectionViewCell. You’re not going to add any code to it yet. You just need to #import it in the view controller’s implementation file.

Open the view controller’s implementation file and create a static NSString instance with some indicative value; you’ll use this as your reuse identifier. Add two instance variables: One is an NSMutableArray representing the model, and the other is an NSDateFormatter that you’ll use to format content to the user (see Listing 2.5).

Listing 2.5 Instance Variables and Static Identifier Setup


#import "AFCollectionViewCell.h"

static NSString *CellIdentifier = @"Cell Identifier";

@implementation AFViewController
{
//This is our model
NSMutableArray *datesArray;
NSDateFormatter *dateFormatter;
}


Next, create a viewDidLoad implementation that sets up an empty model (your datesArray) and a date formatter instance. Also configure your layout and collection view to look pretty, register your UICollectionViewCell subclass for this reuse identifier, and add a button to your navigation bar (see Listing 2.6).

Listing 2.6 Configuring a UICollectionView in viewDidLoad


- (void)viewDidLoad
{
[super viewDidLoad];

//instantiate our model
datesArray = [NSMutableArray array];
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:
[NSDateFormatter dateFormatFromTemplate:@"h:mm:ss a" options:0
locale:[NSLocale currentLocale]]];

//configure our collection view layout
UICollectionViewFlowLayout *flowLayout =
(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
flowLayout.minimumInteritemSpacing = 40.0f;
flowLayout.minimumLineSpacing = 40.0f;
flowLayout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
flowLayout.itemSize = CGSizeMake(200, 200);

//configure our collection view
[self.collectionView registerClass:[AFCollectionViewCell class]
forCellWithReuseIdentifier:CellIdentifier];
self.collectionView.indicatorStyle = UIScrollViewIndicatorStyleWhite;

//configure our navigation item
UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self action:@selector(userTappedAddButton:)];

self.navigationItem.rightBarButtonItem = addButton;
self.navigationItem.title = @"Our Time Machine";
}


Awesome. You could run the application right now, but all you would see is an empty screen with a plus button and a title. So, finish with the view controller code before writing your collection view cell subclass (see Listing 2.7). You need to implement yourUICollectionViewDataSource methods.

Listing 2.7 UICollectionViewDataSource Methods


-(NSInteger)collectionView:(UICollectionView *)collectionView
numberOfItemsInSection:(NSInteger)section
{
return datesArray.count;
}

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

cell.text = [dateFormatter stringFromDate:datesArray[indexPath.item]];

return cell;
}


Right now, this throws a compiler error. Don’t worry, though. After you write the rest of your code, it will work. You need a method to respond to your Add button. Create two methods: one with the selector name you gave the addButton in viewDidLoad, and one that you can call from anywhere in your code to add a new date to datesArray (see Listing 2.8).

Listing 2.8 Configuring a UICollectionView in viewDidLoad


-(void)userTappedAddButton:(id)sender
{
[self addNewDate];
}

-(void)addNewDate
{
[self.collectionView performBatchUpdates:^{
//create a new date object and update our model
NSDate *newDate = [NSDate date];
[datesArray insertObject:newDate atIndex:0];

//update our collection view
[self.collectionView insertItemsAtIndexPaths:
@[[NSIndexPath indexPathForItem:0 inSection:0]]];
} completion:nil];
}


You’re calling performBatchUpdates:completion: on the UICollectionView. This gets you animation (defined by your layout class; more on that in Chapter 3) for free. Amazing! Now all you have to do is write your UICollectionViewCell subclass. Go to the header file you created earlier (see Listing 2.9). You’re going to give it a single, NSString property.

Listing 2.9 UICollectionViewCell Subclass Header


@interface AFCollectionViewCell : UICollectionViewCell

@property (nonatomic, copy) NSString *text;

@end


Now your compiler would stop complaining, but nothing really interesting would happen if you ran the app. Open the implementation file for the cell and add a UILabel instance variable. Override the initWithFrame: method with the implementation in Listing 2.10.

Listing 2.10 UICollectionViewCell Subclass Initialization


@implementation AFCollectionViewCell
{
//subview of our contentView
UILabel *textLabel;
}

#pragma mark - Initialization

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

self.backgroundColor = [UIColor whiteColor];

textLabel = [[UILabel alloc] initWithFrame:self.bounds];
textLabel.textAlignment = NSTextAlignmentCenter;
textLabel.font = [UIFont boldSystemFontOfSize:20];
[self.contentView addSubview:textLabel];

return self;
}


Next, you’re going to override the text property to update the label. You’re also going to override an important method of UICollectionViewCell called prepareForReuse (see Listing 2.11).

Listing 2.11 UICollectionViewCell Reuse


-(void)prepareForReuse
{
[super prepareForReuse];

self.text = @"";
}

-(void)setText:(NSString *)text
{
_text = [text copy];

textLabel.text = self.text;
}


This updates your cell’s label with the string that’s being set as your text property. In prepareForReuse, you call super (very important!) and then set your text to the empty string. This is really important; you need to reset your cell to its starting, neutral state as much as possible. Otherwise, the data source for the collection view might forget to reset parts of it, and you can end up with an inconsistent and confusing user interface.

Run the application, and you see an empty screen. Tap the plus button to add a new cell to the collection view. Notice the animation you get as a new cell is added to the top of the collection view (see Figure 2.8). Nice! You also have rotation support included, for free.

Image

Figure 2.8 Basic cell display example

I don’t want to sound like a broken record about MVC, but it’s important to note that the cell doesn’t have any idea what it is displaying; it’s passed a string that happens to contain a date that corresponds to the model. What’s important is that you’re not passing it the NSDate object itself.

Now that you have a basic collection view example, take a closer look at the UICollectionView class itself. The cell has two Boolean properties of importance: selected and highlighted. The highlighted state depends completely on the user interaction; when the user has her finger pressed down on a cell, it becomes highlighted automatically. Cell selection is less transient; cells become selected (if their collection view supports selection) when the user lifts her finger. Cells stay selected either until some code you’ve written unselects them or until the user taps them again. When being tapped to become either selected or unselected, cells become highlighted temporarily. The setters for these properties can (and often are) called from within animation blocks. Be aware when overriding their implementations that changes you make will likely be implicitly animated.

Selection and highlighting can be confusing. Don’t worry, though, because the next example explores it a little more. In the meantime, Figure 2.9 should help.

Image

Figure 2.9 UICollectionViewCell view hierarchy

In the custom subclass from the last exercise, you added the UILabel subview to self.contentView and not self. In general, you should not add subviews directly to a collection view cell; always add them to its contentView. Here’s why.

UICollectionViewCell has three subviews, denoted in Figure 2.10. The black rectangle, at the back, is the collection view cell itself. The green view at the front is the contentView, and it’s there that you add your subviews. The two intervening views are theselectedBackgroundView and the backgroundView. These are both optional and can be set at any time. The backgroundView, if set, is permanently present.

Image

Figure 2.10 UICollectionViewCell view hierarchy

When the cell becomes selected, the selectedBackgroundView is added to the view hierarchy; when the cell becomes unselected, it is removed. Note that these two events can be called from within animation blocks and the selectedBackgroundView is animated in (with a crossfade by default).

Now that you have a better understanding of the view hierarchy in UICollectionView-Cell, you can continue to another example that helps illustrate the uses of these properties, contentView, and images.

You’re going to create an app that displays 12 images repeated in 10 different sections. Each section is going to have its own background color, unless it is selected, demonstrating how to use the selectedBackgroundView. You’re using 12 images because they fit a single section in one screen full of content.

Create a new Xcode project based on the Empty template. Create a subclass of UICollectionViewController and a subclass of UICollectionViewCell, just like last time. Set up an instance of the view controller as the window’s root view controller in the app delegate—no need to use a navigation controller this time.

Use two arrays to store your models: one for the images and one for the colors you’re using for the background color. You’re going to tweak the cell size, inter-item spacing, and line spacing from the last example. In addition, you’re going to enable multiple selection on the collection view; this is going to enable users to select more than one cell at one time and also enables users to deselect cells by tapping them. Everything in the viewDidLoad method in Listing 2.12 should look familiar. I created a series of JPEG images, named 0.jpg to 11.jpg, for 12 total.

Listing 2.12 Configuring a UICollectionView in viewDidLoad


static NSString *CellIdentifier = @"Cell Identifier";

@implementation AFViewController
{
//models
NSArray *imageArray;
NSArray *colorArray;
}

- (void)viewDidLoad
{
[super viewDidLoad];

//Set up our models
NSMutableArray *mutableImageArray = [NSMutableArray arrayWithCapacity:12];
for (NSInteger i = 0; i < 12; i++)
{
NSString *imageName = [NSString stringWithFormat:@"%d.jpg", i];
[mutableImageArray addObject:[UIImage imageNamed:imageName]];
}
imageArray = [NSArray arrayWithArray:mutableImageArray];

NSMutableArray *mutableColorArray = [NSMutableArray arrayWithCapacity:10];
for (NSInteger i = 0; i < 10; i++)
{
CGFloat redValue = (arc4random() % 255) / 255.0f;
CGFloat blueValue = (arc4random() % 255) / 255.0f;
CGFloat greenValue = (arc4random() % 255) / 255.0f;

[mutableColorArray addObject:[UIColor colorWithRed:redValue
green:greenValue blue:blueValue alpha:1.0f]];
}
colorArray = [NSArray arrayWithArray:mutableColorArray];

//configure our collection view layout
UICollectionViewFlowLayout *flowLayout =
(UICollectionViewFlowLayout *)
self.collectionView.collectionViewLayout;
flowLayout.minimumInteritemSpacing = 20.0f;
flowLayout.minimumLineSpacing = 20.0f;
flowLayout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 10);
flowLayout.itemSize = CGSizeMake(220, 220);

//configure our collection view
[self.collectionView registerClass:[AFCollectionViewCell class]
forCellWithReuseIdentifier:CellIdentifier];
self.collectionView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
self.collectionView.allowsMultipleSelection = YES;
}


Because you’re displaying multiple sections, you need to implement a new, optional UICollectionViewDelegate method called numberOfSectionsIn-CollectionView:. The collectionView:cellForItemAtIndexPath: implementation shown in Listing 2.13 is also going to look familiar.

Listing 2.13 UICollectionViewDataSource Methods


-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return colorArray.count;
}

-(NSInteger)collectionView:(UICollectionView *)collectionView
numberOfItemsInSection:(NSInteger)section
{
return imageArray.count;
}

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

cell.image = imageArray[indexPath.item];
cell.backgroundColor = colorArray[indexPath.section];

return cell;
}


Open the collection view cell subclass and add an UIImageView instance variable to the class. Also add a strong UIImage property named image. Write a new initializer to instantiate the instance variable (see Listing 2.14).

Listing 2.14 UICollectionViewCell Subclass Initialization


@implementation AFCollectionViewCell
{
UIImageView *imageView;
}

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

self.backgroundColor = [UIColor whiteColor];

imageView = [[UIImageView alloc] initWithFrame:
CGRectInset(self.bounds, 10, 10)];
[self.contentView addSubview:imageView];

UIView *selectedBackgroundView = [[UIView alloc] initWithFrame:CGRectZero];
selectedBackgroundView.backgroundColor = [UIColor whiteColor];
self.selectedBackgroundView = selectedBackgroundView;

return self;
}


You’re moving in the frame of the image view by 10 points to create a border around the image. Override the setImage: method to set the image of the imageView. You’re also going to fill in a prepareForReuse implementation and include an implementation forsetHighlighted: (see Listing 2.15). Also notice that you’ve set your selected-BackgroundView to a plain white view. This white view will be placed in front of the cell (and in front of any background view, which you don’t have in this case) while the cell is selected.

Listing 2.15 UICollectionViewCell Overridden Methods


-(void)prepareForReuse
{
[super prepareForReuse];

self.backgroundColor = [UIColor whiteColor];
self.image = nil; //also resets imageView's image
}

-(void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];

if (highlighted)
{
imageView.alpha = 0.8f;
}
else
{
imageView.alpha = 1.0f;
}
}

-(void)setImage:(UIImage *)image
{
_image = image;

imageView.image = image;
}


Remember to always call super’s implementation of overridden properties (unless you purposefully don’t want to and have a really good reason). In the implementation, you decrease the alpha of your image view to 80% when it is highlighted. Run the application.

Play around with the application. Tap cells to make them selected, and then tap them again. Notice that if you tap and drag, the collection view cancels your tap and scrolls instead. This is because the UIScrollView property canCancelContentTouches is set to YES. Also notice how the collection view delays highlighting the cell until you hold down the touch for a few tenths of a second. This is because the UIScrollView property delaysContentTouches is set to YES. In the viewDidLoad implementation, play with these two methods to experiment with how they affect the user experience of the collection view (and, in fact, all scroll views, as these are the default values).

Note a couple of things about the selectedBackgroundView and backgroundView properties of UICollectionViewCell. First, they will be stretched to fit to whatever cell they’re assigned. This is why you were able to initialize the selected background view in this example with a frame of CGRectZero. Next, some attributes, like alpha, will be reset to their defaults (in alpha’s case, 1.0f) by the collection view. Be aware of these issues when troubleshooting display problems with backgrounds of cells.

If you want proof that the selectedBackgroundView is placed within the view hierarchy, you can set it to have a slightly transparent color. Change the background color of the selectedBackgroundView to something like [UIColor colorWithWhite:1.0f alpha:0.8f]. Now you’ll be able to see through the selectedBackground view to its superview, the collection view itself.

Before the chapter wraps up with a case study on performance, I want to make a quick diversion to revisit storyboards and .xib files. Now that you understand how collection view cells work and you can create subclasses to customize their appearance, take a look at how to approach the previous exercise using storyboards and .xibs.

Using .xibs is the most similar to code, so start with that. Open the Xcode project from the previous exercise (copy it first if you’re not using source control) and add a new file.

Under the left pane of the new file dialog, select User Interface, and then double-tap the Empty file to create a new, empty .xib. Give it the same name as your collection view cell subclass. In the object library, find Collection View Cell and drag it onto the empty canvas.

Select the new cell and open the Size Inspector and set the sizes to 220 wide and 220 tall. These are going to be reconfigured by the collection view anyway, so it’s only to help us get a visual sense of what the cell will look like. Open the Attributes Inspector and make the background color white. Open the Identity Inspector and set the type of the collection view cell to your subclass.

In the subclass, you need to remove a lot of code. The initWithFrame: initializer will no longer be called. Create a new method called awakeFromNib. This method is called when an instance of the class is “thawed” from the nib. In this method, you place your customselectedBackgroundView initialization. See how some things need to be done with code, anyway?

Drag an image view onto the cell. Set the springs and struts (or Autolayout constraints) so that the image view is inset by 10 points on all sides. Move the instance variable to the header file and prefix it with the keyword IBOutlet so that the .xib can see it. Command-click and drag from the collection view cell to the image view inside it; select the imageView outlet from the menu that appears.

Finally, you need to tell the collection view to use this nib instead of initializing its own copies of the collection view cell subclass itself. In viewDidLoad, change the setup for the collection view, as shown in Listing 2.16.

Listing 2.16 UICollectionViewCell Registration Using UINib


-(void)viewDidLoad
{
[super viewDidLoad];

//all that other stuff

[self.collectionView registerNib:
[UINib nibWithNibName:@"AFCollectionViewCell" bundle:nil]
forCellWithReuseIdentifier:CellIdentifier];
}


Run the application and see that it behaves exactly as it did with code. Notice that even though you’re using UINib, you are still forced to use a subclass implementation file.

Finally, you’re going to use storyboards to produce the same effect. Empty the applicationDidFinishLaunchingWithOptions: implementation to just return YES. Delete the .xib file. Pare down the viewDidLoad implementation to look like Listing 2.17.

Listing 2.17 UICollectionViewCell Registration Using Storyboards


- (void)viewDidLoad
{
[super viewDidLoad];

//Set up our models
NSMutableArray *mutableImageArray = [NSMutableArray arrayWithCapacity:12];
for (NSInteger i = 0; i < 12; i++)
{
NSString *imageName = [NSString stringWithFormat:@"%d.jpg", i];
[mutableImageArray addObject:[UIImage imageNamed:imageName]];
}
imageArray = [NSArray arrayWithArray:mutableImageArray];

NSMutableArray *mutableColorArray = [NSMutableArray arrayWithCapacity:10];
for (NSInteger i = 0; i < 10; i++)
{
CGFloat redValue = (arc4random() % 255) / 255.0f;
CGFloat blueValue = (arc4random() % 255) / 255.0f;
CGFloat greenValue = (arc4random() % 255) / 255.0f;

[mutableColorArray addObject:
[UIColor colorWithRed:redValue
green:greenValue blue:blueValue alpha:1.0f]];
}
colorArray = [NSArray arrayWithArray:mutableColorArray];

//configure our collection view
self.collectionView.allowsMultipleSelection = YES;
}


Your viewDidLoad now only sets up the models and sets one property on the collection view that can’t be set with the Attributes Inspector of a storyboard.

Create a new storyboard file called MainStoryboard. Open the Xcode project settings and set MainStoryboard as the Main Storyboard. (You read that correctly, folks.) Drag a UICollectionViewController onto the empty storyboard and set its custom class to be the one your code lives in. Expand the view hierarchy of the collection view until you get to the collection view cell. Set its custom class to be your UICollectionViewCell subclass and set its Reuse Identifier to Cell Identifier in the Attributes Inspector. Open the Size inspector and set it to 220 wide by 220 tall.

Add an image view as a subview to the cell; command-click and drag from the cell to the image view to set the imageView outlet of the cell. Set the image view to 200 wide by 200 tall and set its springs and struts (or Autolayout constraints) so that it grows with the cell.

Finally, click the Collection View Flow Layout object in the view hierarchy. Set the Min Spacing and Section Insets to those you previously used in code (see Figure 2.11). Run the application.

Image

Figure 2.11 The same example, using storyboards

The advantage to using storyboards or .xib files is that you get to lay out your interface visually. This can be a lot of help when working with a designer or when first learning about view hierarchies in CocoaTouch. However, storyboards and .xib files don’t offer a lot of compelling advantages, other than their visual nature. There are two problems with storyboards and collection views: the tight coupling between the collection view cell (its reuse identifier) and the code, and some tricky debugging when you modify the settings of the storyboard in code at runtime. You can’t rely on what you see visually at compile time because it’s likely going to be changed by code at runtime, anyway.

From this point forward, I don’t devote any more attention to .xib files or storyboards. You’ve seen how they work, so if you’re incorporating them into an existing project that uses them, you’ll be able to apply the techniques in this book. Even if you’re still getting used to laying out interface in code instead of visually, I encourage you to use .xib files rather than storyboards. Remember to use custom UICollectionViewCell subclasses to keep your code loosely coupled; your view controller shouldn’t know about the internals of the cell’s view hierarchy.

Now that you have a good understanding of UICollectionViewCell and how to display content to users, let’s take a look at performance.

Case Study: Evaluating Performance of UICollectionView

When you evaluate the performance of any iOS app, you must measure it on an actual device. The specific device matters a little bit, but it’s most important that you don’t rely on the simulator. Although it is useful for a lot of things, like NSZombies, the simulator has an entire personal computer powering it, which isn’t the case for most users’ iPhones.

Choosing a test device can be a little tricky. Obviously, something really new like an iPhone 5 would not be an ideal choice for testing performance of your app when strained. However, don’t rely on using the oldest or slowest hardware, either. Although the iPhone 3GS only has one core, it can perform much better than the iPhone 4 in practice because the iPhone 4 has more random access memory (RAM) and a multicore central processing unit (CPU), but has to push out four times as many pixels to its Retina screen.

Beyond the iPhone, you also have to consider the iPhone touch. If you’re writing an app that pushes the limits of the device, you should be testing on all hardware/software combinations. However, many iOS developers are just single-person operations whipping up some cool apps who don’t have thousands of dollars to spend on testing hardware (or understanding significant others who are willing to indulge our addiction to Apple products). If you don’t have an old iPhone lying around, an iPod touch works well and is inexpensive.

With regard to collection views and other scroll views, the most important aspect of performance is perceived scrolling responsiveness. Notice that I said perceived responsiveness. You measure this by measuring the screen refresh rate. Ideally, this rate would be 60 frames per second (fps), the native refresh rate. That means that during each invocation of the main run loop, your application has only has 16 milliseconds to complete. That’s not a lot of time. This case study is going to highlight the places that inefficient code severely affects performance and shows you how to restructure your code to keep it lean. Open the Performance Problems Example project in the same code. (The solutions are also there, with the prefix “Solved.”)

Here’s one more tip before getting to the actual profiling: While you are measuring the performance of your app, the performance of the CPU will be hampered by Instruments (kind of like an observer effect). To avoid this, open Preferences in Instruments and check the Always Use Deferred Mode check box. This collects the data locally on the device and doesn’t send it to the computer until the run has completed.

After you have your device and have jumped through Apple’s hoops to run your app on it, connect it to your computer. Make sure that your device is selected from the Scheme drop-down menu. In Xcode, open the Product menu and select Profile (Command-I). This builds your application with Release build settings (like compiler optimizations) and opens the Instruments template chooser (see Figure 2.12).

Image

Figure 2.12 Instruments template chooser

Remember that you get different templates depending on if you’re using the simulator or an actual device. Choose the Core Animation template. This gives you the screen refresh rate as well as the CPU usage, which tells you where the CPU is spending the majority of its time executing code. Click Profile and scroll the app. Use the scrolling and notice how awful the responsiveness is. When you’re satisfied that this is really, really bad code, click the Stop button to see the results in Instruments (see Figure 2.13).

Image

Figure 2.13 Core Animation profiler template results

Holy screen refresh rates, Batman! A peak of only 37fps is terrible. Select the Time Profiler and open the Extended Detail pane (see Figure 2.14).

Image

Figure 2.14 Core Animation profiler template results Extended Detail pane

You can see that the most amount of time is being spent downloading the images from the Internet on the main thread. Never a good idea! Furthermore, you’re not caching the downloaded data anywhere. Add an NSCache instance to your view controller to hold your cached data results. This class is a handy little key/value store that automatically evicts items when memory becomes low. Initialize it in loadView (see Listing 2.18).

Listing 2.18 Downloading Images in a Background Thread


-(void)configureCell:(AFCollectionViewCell *)cell atIndexPath:(NSIndexPath
*)indexPath withURLString:(NSString *)urlString
{
//Try to pull out a cached NSData instance from our cache
id data = [photoDataCache objectForKey:urlString];

if (data)
{
//This branch executes if the objectForKey: is non-nil,
//meaning we have downloaded the image before.


if ([data isKindOfClass:[NSNull class]])
{
//This indicates that the instance is NSNull, so we
//should not use it.

//nop
}
else
{
//We can successfully decompress our JPEG data
UIImage *image = [UIImage imageWithData:data];
[cell setImage:image];
}
}
else
{
//Download the image in a background queue
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSData *data = [self downloadImageDataWithURLString:urlString];

//Now that we have the data, dispatch back to the main queue
//to use it. UIImage is part of UIKit and can *only* be
//accessed on the main thread
dispatch_async(dispatch_get_main_queue(), ^{

UIImage *image = [UIImage imageWithData:data];

if (image)
{
//This cell instance passed in as a parameter might
//have been reused by now. Call
//reloadItemsAtIndexPaths: instead.

[photoDataCache setObject:data forKey:urlString];
[photoCollectionView reloadItemsAtIndexPaths:@[indexPath]];
}
else
{
//This indicates the JPEG decompression failed.
//Set NSNull in our cache
[photoDataCache setObject:[NSNull null] forKey:urlString];
}
});
});
}
}


This code is mostly straightforward. Notice that you added a new parameter, an index path, to the method signature. This is used to reload the item at that index path; referencing the cell directly is unsafe because it might have been reused. You can run into problems reloading the item directly if the cell has been deleted, for instance. This simple example fits the needs of this chapter. If you’re doing anything more complicated, I suggest relying on a fetched results controller to update the collection view.


Note

There are better ways to cache photos, like Core Data. There are also better ways to download data from the Internet, but the point of this case study is to examine problems with collection view performance, not general software architecture.


Rerun the profiler with the modified code. You can see that the performance has improved significantly. That’s good, but take a closer look to see whether you can improve things even more. If you take a look at the Extended Detail pane, the method that is taking up the most time is stillconfigureCell:atIndexPath:withURLString:. It looks like imageWithData: is taking up a lot of CPU time.

You could take a few approaches to deal with this. You could cache the decompressed JPEGs, which is a good idea, but it has its drawbacks. The biggest problem is that it consumes a lot of memory. Your images are 145 by 145 pixels and have 3 channels at 8 bits a channel. That means that after each image is decompressed, it takes up 145 * 145 * 3 = 63KB. That doesn’t sound like a lot, but the app is running on memory-constrained devices, and the OS will kill the app if it uses too much memory.

Instead, decompress the JPEG data on the background queue. “Ha!” you say, “UIImage is part of UIKit, and telling me to use it on the background queue is a fool’s errand!” You’re not wrong, but an alternative exists. UIImage is a handy class, but is quite opaque in terms of telling us if it has decompressed the image already. For instance, use the Core Graphics to decompress the image (see Listing 2.19).

Listing 2.19 Category on NSData to Decompress JPEG Data


//Place this line in an external header file
typedef void (^JPEGWasDecompressedCallback)(UIImage *decompressedImage);

-(void)af_decompressedImageFromJPEGDataWithCallback:
(JPEGWasDecompressedCallback)callback
{
uint8_t character;
[self getBytes:&character length:1];

if (character != 0xFF)
{
//This is not a valid JPEG.

callback(nil);

return;
}

// get a data provider referencing the relevant file
CGDataProviderRef dataProvider =
CGDataProviderCreateWithCFData((__bridge CFDataRef)self);

// use the data provider to get a CGImage; release the data provider
CGImageRef image =
CGImageCreateWithJPEGDataProvider(dataProvider, NULL, NO,
kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);

// make a bitmap context of a suitable size to draw to, forcing decode
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
size_t bytesPerRow = roundUp(width * 4, 16);
size_t byteCount = roundUp(height * bytesPerRow, 16);

void *imageBuffer = malloc(byteCount);

if (width == 0 || height == 0)
{
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil);
});
}

CGColorSpaceRef colourSpace = CGColorSpaceCreateDeviceRGB();

CGContextRef imageContext =
CGBitmapContextCreate(imageBuffer, width, height, 8, bytesPerRow,
colour kCGImageAlphaNone | kCGImageAlphaNoneSkipLast);
//Despite what the docs say these are not the same thing

CGColorSpaceRelease(colourSpace);

// draw the image to the context, release it
CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), image);
CGImageRelease(image);

// now get an image ref from the context
CGImageRef outputImage = CGBitmapContextCreateImage(imageContext);

CGContextRelease(imageContext);
free(imageBuffer);

dispatch_async(dispatch_get_main_queue(), ^{
UIImage *decompressedImage = [UIImage imageWithCGImage:outputImage];
callback(decompressedImage);
CGImageRelease(outputImage);
});
}


This category is very useful. Call the decompression method on a background queue and everything is taken care of for you: The NSData instance is decompressed, if it is in fact a JPEG, on the queue that the method is invoked from. When the decompression is complete, it invokes a callback block and takes care of cleaning up the CGImageRef memory.

Now that you can safely decompress JPEGs on the background queue, incorporate that into your code, as in Listing 2.20.

Listing 2.20 Decompressing Images in a Background Thread


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[data af_decompressedImageFromJPEGDataWithCallback:
^(UIImage *decompressedImage) {
[cell setImage:decompressedImage];
}];
});


Because JPEG decompression takes only a few milliseconds, I’m updating the cell directly in Listing 2.20. If you’re decompressing JPEGs that are megabytes large, this isn’t going to work for you, but displaying images that large in a collection view is a bad idea, generally.

If you rerun Instruments, you see that the most expensive operation, overall, is allocating space from UICollectionViewCell’s initWithFrame:. This is really good. Anecdotally, the app runs way smoother.

High five! But take a look at two other parts of the codebase that could be improved.

The images are 145 by 145 pixels, but your cells are 145 by 100 logical pixels. UIImageView is scaling the images down to fit. You could change the content mode to center them, instead, so that they aren’t scaled.

Ideally, your image size and cell size should be the same so that the OS doesn’t have to resize anything, which improves performance. However, if you’re using a third-party API, you won’t have control over the image size.

The only other thing I could recommend to improve performance of this example is to turn on the masksToBounds property of the cell’s layer; you used this property in conjunction with the cornerRadius. This causes a strain on the CPU because it requires offscreen rendering passes and can cause a lot of problems. It’s something to check if you can’t figure out why your collection view is slow.

If the collection view background is opaque, you can use a PNG in a UIImageView subview of the contentView to mask out the corners. This is a good approach, too, but don’t use resizable images if you can avoid doing so. If all your cells are the same size, use a nonresizableUIImage to mask the corners because it renders faster.

That’s all for the first performance example. Take a look at the next example, called “Performance Problems Example II,” in the same code. Build and run the app to get a feel for how it works.

The use case for this app is the following: You’ve been hired by an up-and-coming startup that just got some angel funding and who are building a social network for cats (see Figure 2.15). You are to prototype the “Facebook Wall” equivalent of their future mobile app so that they can grab millions in venture capital funding.

Image

Figure 2.15 A social network for cats

The app displays comments in cells that have different background colors. The model is set up in setupModel. Just ignore this method; it is not relevant to the case study. Also, notice how Xcode lets you use emoji in Objective-C source code. How cool is that?

Profile the app and use the same Core Animation template as the last example. The peak frame rate is 48fps, which isn’t terrible, but not ideal. When you open the Extended Detail pane, what you see should cause some alarm (see Figure 2.16).

Image

Figure 2.16 First profiling run in Instruments

The most expensive operation performance is unloading the .xib file. What’s with that? Open AFCollectionViewCell.xib and bask in the sheer existential horror of the view hierarchy.

Obviously, Figure 2.17 represents a pedagogical example. You would never have a view hierarchy in a nib quite this bad. Even though you have a quite complex view hierarchy, all you’re really doing is displaying some text on a colored background. You can draw this, and the equivalent of all those useless views, a lot faster in drawRect:. You can apply the same logic in your own cell subclasses; if you have a complex view hierarchy that takes too long to draw, implement drawRect: and ditch the view hierarchy. drawRect: can also be a slow performer, however, and using it in a simple example like this is only to illustrate how it’s done. You should use it only when drawing components of your view manually is faster than rendering a necessarily complex view hierarchy.

Image

Figure 2.17 Crazy view hierarchy

Delete the .xib and the two properties from the cell’s header file. Instead of registering a UINib in the view controller’s viewDidLoad, register a Class. Instead of using a separate background view for the color, just draw it in drawRect:. Create a new string property for the cell’s text. You’re going to override the getter and setter for backgroundColor to do some clever drawing (see Listing 2.21).

Listing 2.21 New Cell Subclass


static inline void addRoundedRectToPath(CGContextRef context, CGRect rect, float
ovalWidth, float ovalHeight)
{
float fw, fh;
if (ovalWidth == 0 || ovalHeight == 0) {
CGContextAddRect(context, rect);
return;
}
CGContextSaveGState(context);
CGContextTranslateCTM (context, CGRectGetMinX(rect), CGRectGetMinY(rect));
CGContextScaleCTM (context, ovalWidth, ovalHeight);
fw = CGRectGetWidth (rect) / ovalWidth;
fh = CGRectGetHeight (rect) / ovalHeight;
CGContextMoveToPoint(context, fw, fh/2);
CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1);
CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1);
CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1);
CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1);
CGContextClosePath(context);
CGContextRestoreGState(context);
}

@implementation AFCollectionViewCell
{
UIColor *realBackgroundColor;
}

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

self.opaque = NO;
self.backgroundColor = [UIColor clearColor];

return self;
}

-(void)prepareForReuse
{
...
//Not relevant to this part of the case study
}

-(void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();

CGContextSaveGState(context);

[realBackgroundColor set];

addRoundedRectToPath(context, self.bounds, 10, 10);
CGContextClip(context);

CGContextFillRect(context, self.bounds);

CGContextRestoreGState(context);

[[UIColor whiteColor] set];

[self.text
drawInRect:CGRectInset(self.bounds, 10, 10)
withFont:[UIFont boldSystemFontOfSize:20]
lineBreakMode:NSLineBreakByWordWrapping
alignment:NSTextAlignmentCenter];
}

#pragma mark - Overridden Properties

-(void)setBackgroundColor:(UIColor *)backgroundColor
{
[super setBackgroundColor:[UIColor clearColor]];

realBackgroundColor = backgroundColor;

[self setNeedsDisplay];
}

-(UIColor *)backgroundColor
{
return realBackgroundColor;
}

-(void)setText:(NSString *)text
{
_text = [text copy];

[self setNeedsDisplay];
}

@end


The addRoundedRectToPath C method is handy and can be easily modified to only round certain corners. These methods should usually be placed in a separate source file so that they can be reused. Take a look at Listing 2.22.

Listing 2.22 Decompressing Images in a Background Thread


-(void)configureCell:(AFCollectionViewCell *)cell withModel:(AFModel *)model
{
cell.backgroundColor = [model.color colorWithAlphaComponent:0.6f];
cell.text = model.comment;
}


Listing 2.22 is an efficient implementation. The drawing code is straightforward and the code using the cell works well within the MVC architecture. Reprofile the application.

Huh. Figure 2.18 shows there’s some method called performLongRunningTask being called when you dequeue a cell. It’s severely hampering the frame refresh rate. You shouldn’t be performing long-running tasks on the main thread, and if you look at the code, it’s being called fromprepareForReuse. The developer has conflated preparation for reuse with having shown the user the cell. This is really not acceptable. Refactor this logic into the view controller instead (see Listing 2.23).

Image

Figure 2.18 First profiling run in Instruments


Note

A little trick is going on. I have an empty for loop in performLongRunningTask to cause performance problems on purpose, but to get this example to work, I’ve had to disable compiler optimizations. LLVM is too smart, and it strips out the empty loop if compiler optimizations are enabled.


Listing 2.23 Refactoring Code out of the Cell Subclass


-(void)collectionView:(UICollectionView *)collectionView
didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath
*)indexPath
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
[self performLongRunningTask];
});
}

-(void)performLongRunningTask
{
/*
Let's run some long-running task. Maybe
it's some complicated view hierarchy math that
could be simplified with Autolayout.
*/
for (int i = 0; i < 5000000; i++);
}


Now the code to invoke the long-running task is in the appropriate place and the task is performed on a background queue. Great. Reprofile the app. The frame rate is something around 55fps, which is pretty good. The slowest part of the code is drawRect:, which can causes some performance problems. As stated earlier, drawRect: is only a good route to take when you have a necessarily complex view hierarchy.