Subclassing UITableViewCell - iOS Programming: The Big Nerd Ranch Guide (2014)

iOS Programming: The Big Nerd Ranch Guide (2014)

19. Subclassing UITableViewCell

A UITableView displays a list of UITableViewCell objects. For many applications, the basic cell with its textLabel, detailTextLabel, and imageView is sufficient. However, when you need a cell with more detail or a different layout, you subclass UITableViewCell.

In this chapter, you will create a custom subclass of UITableViewCell named BNRItemCell that will display BNRItem instances more effectively. Each of these cells will show a BNRItem’s name, its value in dollars, its serial number, and a thumbnail of its image (Figure 19.1).

Figure 19.1 Homepwner with subclassed table view cells

Homepwner with subclassed table view cells

Creating BNRItemCell

UITableViewCell is a UIView subclass. When subclassing UIView (or any of its subclasses), you often override its drawRect: method to customize the view’s appearance. However, when subclassing UITableViewCell, you usually customize its appearance by adding subviews to the cell. You do not add them directly to the cell though; instead you add them to the cell’s content view.

Each cell has a subview named contentView, which is a container for the view objects that make up the layout of a cell subclass (Figure 19.2). When you subclass UITableViewCell, you often change its look and behavior by changing the subviews of the cell’s contentView. For instance, you could create instances of the classes UITextField, UILabel, and UIButton and add them to the contentView.

Figure 19.2 UITableViewCell hierarchy

UITableViewCell hierarchy

Adding subviews to the contentView instead of directly to the cell itself is important because the cell will resize its contentView at certain times. For example, when a table view enters editing mode the contentView resizes itself to make room for the editing controls (Figure 19.3). If you were to add subviews directly to the UITableViewCell, these editing controls would obscure the subviews. The cell cannot adjust its size when entering edit mode (it must remain the width of the table view), but the contentView can resize, and it does.

(By the way, notice the UIScrollView in the cell hierarchy? That is how iOS moves the contents of the cell to the left when it enters editing mode. You can also use a right-to-left swipe on a cell to show the delete control, and this uses that same scroll view to get the job done. It makes sense then that the contentView is a subview of the scroll view.)

Figure 19.3 Table view cell layout in standard and editing mode

Table view cell layout in standard and editing mode

Open Homepwner.xcodeproj. Create a new NSObject subclass and name it BNRItemCell.

In BNRItemCell.h, change the superclass to UITableViewCell.

@interface BNRItemCell : NSObject

@interface BNRItemCell : UITableViewCell

Configuring a UITableViewCell subclass’s interface

The easiest way to configure a UITableViewCell subclass is with a XIB file. Create a new Empty XIB file and name this file BNRItemCell.xib. (The Device Family is irrelevant for this file.)

This file will contain a single instance of BNRItemCell. When the table view needs a new cell, it will create one from this XIB file.

In BNRItemCell.xib, select BNRItemCell.xib and drag a UITableViewCell instance from the object library to the canvas. (Make sure you choose UITableViewCell, not UITableView or UITableViewController.)

Select the Table View Cell in the outline view and then the identity inspector (the Configuring a UITableViewCell subclass’s interface tab). Change the Class to BNRItemCell (Figure 19.4).

Figure 19.4 Changing the cell class

Changing the cell class

A BNRItemCell will display three text elements and an image, so drag three UILabel objects and one UIImageView object onto the cell. Configure them as shown in Figure 19.5. Make the text of the bottom label a slightly smaller font and a dark shade of gray.

Figure 19.5 BNRItemCell’s layout

BNRItemCell’s layout

Exposing the properties of BNRItemCell

In order for BNRItemsViewController to configure the content of a BNRItemCell in tableView:cellForRowAtIndexPath:, the cell must have properties that expose the three labels and image view. These properties will be set through outlet connections in BNRItemCell.xib.

The next step, then, is to create and connect outlets on BNRItemCell for each of its subviews. You will use the same technique you have been using in the last few chapters – Control-dragging from the XIB file into the source file to create the outlets.

Option-click on BNRItemCell.h while BNRItemCell.xib is open. Control-drag from each subview to the method declaration area in BNRItemCell.h. Name each outlet and configure the other attributes of the connection as shown in Figure 19.6. (Pay attention to the Connection, Storage, and Object fields.)

Figure 19.6 BNRItemCell connections

BNRItemCell connections

Double-check that BNRItemCell.h looks like this:

@interface BNRItemCell : UITableViewCell

@property (weak, nonatomic) IBOutlet UIImageView *thumbnailView;

@property (weak, nonatomic) IBOutlet UILabel *nameLabel;

@property (weak, nonatomic) IBOutlet UILabel *serialNumberLabel;

@property (weak, nonatomic) IBOutlet UILabel *valueLabel;

@end

Note that you did not specify the File's Owner class or make any connections with it. This is a little different than your usual XIB files where all of the connections happen between the File's Owner and the archived objects. To see why, let’s see how cells are loaded into the application.

Using BNRItemCell

In BNRItemsViewController’s tableView:cellForRowAtIndexPath: method, you will create an instance of BNRItemCell for every row in the table.

In BNRItemsViewController.m, import the header file for BNRItemCell so that BNRItemsViewController knows about it.

#import "BNRItemCell.h"

Previously, you registered a class with the table view to inform it which class should be instantiated whenever it needs a new table view cell. Now that you are using a custom NIB file to load a UITableViewCell subclass, you will register that NIB instead.

In BNRItemsViewController.m, modify viewDidLoad to register BNRItemCell.xib for the "BNRItemCell" reuse identifier.

- (void)viewDidLoad

{

[super viewDidLoad];

[self.tableView registerClass:[UITableViewCell class]

forCellReuseIdentifier:@"UITableViewCell"];

// Load the NIB file

UINib *nib = [UINib nibWithNibName:@"BNRItemCell" bundle:nil];

// Register this NIB, which contains the cell

[self.tableView registerNib:nib

forCellReuseIdentifier:@"BNRItemCell"];

}

The registration of a NIB for a table view is not anything fancy: the table view simply stores the UINib instance in an NSDictionary for the key "BNRItemCell". A UINib contains all of the data stored in its XIB file, and when asked, can create new instances of the objects it contains.

Once a UINib has been registered with a table view, the table view can be asked to load the instance of BNRItemCell when given the reuse identifier "BNRItemCell".

In BNRItemsViewController.m, modify tableView:cellForRowAtIndexPath:.

- (UITableViewCell *)tableView:(UITableView *)tableView

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

UITableViewCell *cell =

[tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"

forIndexPath:indexPath];

// Get a new or recycled cell

BNRItemCell *cell =

[tableView dequeueReusableCellWithIdentifier:@"BNRItemCell"

forIndexPath:indexPath];

NSArray *items = [[BNRItemStore sharedStore] allItems];

BNRItem *item = items[indexPath.row];

cell.textLabel.text = item.description;

// Configure the cell with the BNRItem

cell.nameLabel.text = item.itemName;

cell.serialNumberLabel.text = item.serialNumber;

cell.valueLabel.text =

[NSString stringWithFormat:@"$%d", item.valueInDollars];

return cell;

}

First, the reuse identifier is updated to reflect your new subclass. The code at the end of this method is fairly obvious – for each label on the cell, set its text to some property from the appropriate BNRItem. (You will deal with the thumbnailView later.)

Build and run the application and create a new BNRItem. The cells load, but the layout of each cell is most likely off. You have not set up constraints for each of the subviews of the cell’s contentView, so let’s do that now.

Constraints for BNRItemCell

BNRItemsViewController’s table view will change its size to match the size of the window. When a table view changes its width, each of its cells also change their width to match. Thus, you need to set up constraints in the cell that account for this change in width. (The height of a cell will not change unless you explicitly ask the table view to change it, either through the property rowHeight or its delegate method tableView:heightForRowAtIndexPath:.)

Right now, BNRItemCell.xib has an initial position and size for each view. For example, the thumbnailView’s size in the XIB file is 40 points wide and 40 points tall (if yours is not exactly 40x40, do not worry; it will be soon.). However, Auto Layout does not care about how big a view is when it is first created; it only cares about what the constraints say. If you were to add a constraint to the image view that pins its width to 500 points, the width would be 500 points – the original size does not factor in.

Here are the constraints you need:

1. Make the UIImageView 40x40 points, close to the left edge of the content view and vertically centered with the content view.

2. Make sure both the nameLabel and serialNumberLabel stay the same fixed distance away from the image view, stretch to fill the length of the cell (minus the size of the image view and the valueLabel), and maintain their vertical stacking.

3. Keep the valueLabel centered vertically with the content view on the right edge of the cell and a fixed distance away from the other two labels.

First, pin the width and height of imageView. You can use the Pin menu or Control-click and drag diagonally from the image view to itself.

Next, center the image view within its container. You can use the Align menu to do this (selecting Vertical Center in container) or Control-click and drag from the image view to the container (selecting Center Vertically in Container). If you Control-click and drag, make sure you do not drag to another subview. An easy way to make sure you drag to the correct view is to drag to the Content View in the document outline (Figure 19.7).

Figure 19.7 Dragging to the document outline

Dragging to the document outline

Let’s set up the horizontal constraints for all of the views at once. Select the four subviews and then, from the Pin menu, check the left and right struts at the top. Click Add 6 Constraints. The image view now has all of the constraints that it needs, as evident by its blue constraint lines. (If yours are not blue yet, do not worry. Your image view’s frame may not match its constraints, and you will make sure this is fixed soon.) The completed constraints for the image view are shown in Figure 19.8.

Figure 19.8 Image view constraints

Image view constraints

Now, finish the constraints for nameLabel and serialNumberLabel. Select these two labels and open the Pin menu. Check the top and bottom strut, as well as Height, and click Add 5 Constraints. The constraints for those two labels will turn blue, but there could be unsatisfiable constraints if the table view cell’s height ever changes. Why? Right now, all of the vertical constraints that were added have their equality set to Equal. In the visual format language, this gives the equation:

V:|-1-[nameLabel(==21)]-5-[serialNumberLabel(==15)]-1-|

Notice that these add up to 43, exactly the current height of the content view. If the height of the table view cell changes, one of these constraints will have to break. (Want to see this? Go ahead and change the height of the cell from the Size Inspector. It is probably a good idea to change it back after you see the effects, but it is not absolutely necessary.) You will fix this by changing the relation of one of the constraints to be greater than or equal.

The constraint that you will modify is the one that pins the bottom of nameLabel to the top of serialNumberLabel. While you could select this constraint in the canvas, it is very small and difficult to select. Instead, select nameLabel, and then open up its Size inspector.

Here you can see all of the constraints that affect the selected view. Click on the gear icon associated with the Bottom Space constraint. This will pop up a menu allowing you to Select and Edit... or Delete the constraint. Choose Select and Edit.... At the top of the Attributes inspector, change the Relation to Greater Than or Equal. The constraints for these two labels are shown in Figure 19.9.

Figure 19.9 Name and serial number label constraints

Name and serial number label constraints

Next, select valueLabel and select and add the constraint for Vertical Center in Container from the Align menu.

You need to make one last change to get rid of an ambiguous layout. Since all three labels do not have their widths pinned, they all want to be the width of their intrinsicContentSize. Since the width of the two labels in the middle + the spacing + the width of the value label is greater than their intrinsic widths + that spacing, something will have to give: either the width of nameLabel and serialNumberLabel will have to grow, or the width of valueLabel will have to grow.

Let’s fix this ambiguity by adjusting the priority of the value label’s Content Hugging Priority to be higher than that of the other two labels. This is a better approach than pinning the width of the value label. If the width is pinned, then text longer than can be displayed will be truncated. By increasing the Content Hugging Priority, the value label’s width will always be exactly the width needed to display all of the text. (Unless the label is so long that the text must be truncated to satisfy all the constraints.)

Select valueLabel and open the Size Inspector. Change the Horizontal Content Hugging Priority to be 1000. The finished valueLabel constraints are shown in Figure 19.10.

Figure 19.10 Value label constraints

Value label constraints

You are done! All of the subviews should have blue constraint lines. If any do not, select the cell and then click Update All Frames in Item Cell from the Resolve Auto Layout Issues menu.

It was a bit of work to configure the cell, but its contents will now scale elegantly if the size of the cell changes or if the textual content changes.

Image Manipulation

Now let’s address the thumbnailView’s contents in BNRItemCell. To display an image within a cell, you could just resize the large image of the item from the image store. However, doing so would incur a performance penalty because a large number of bytes would need to be read, filtered, and resized to fit within the cell. A better idea is to create and use a thumbnail of the image instead.

To create a thumbnail of a BNRItem image, you are going to draw a scaled-down version of the full image to an offscreen context and keep a reference to that new image inside a BNRItem instance. You also need a place to store this thumbnail image so that it can be reloaded when the application launches again.

In Chapter 11, you put the full-sized images in the BNRImageStore so that they can be flushed if necessary. However, the thumbnail images will be small enough that you can archive them with the other BNRItem properties.

Open BNRItem.h. Declare a new property for a thumbnail and a new method that will configure that thumbnail.

@property (nonatomic, copy) NSString *itemKey;

@property (nonatomic, strong) UIImage *thumbnail;

- (void)setThumbnailFromImage:(UIImage *)image;

@end

When an image is chosen for a BNRItem, you will give that image to the BNRItem. It will chop it down to a much smaller size and then keep that smaller-sized image as its thumbnail.

The method that will do this is setThumbnailFromImage:. This method will take a full-sized image, create a smaller representation of it in an offscreen graphics context object, and set the thumbnail property to the image produced by the offscreen context. (If you do not know what a graphics context object is, read the section called “For the More Curious: Core Graphics” in Chapter 4).

iOS provides a convenient suite of functions to create offscreen contexts and produce images from them. To create an offscreen image context, you use the function UIGraphicsBeginImageContextWithOptions. This function accepts a CGSize structure that specifies the width and height of the image context, a scaling factor, and whether the image should be opaque. When this function is called, a new CGContextRef is created and becomes the current context.

To draw to a CGContextRef, you use Core Graphics, just as though you were implementing a drawRect: method for a UIView subclass. To get a UIImage from the context after it has been drawn, you call the function UIGraphicsGetImageFromCurrentImageContext.

Once you have produced an image from an image context, you must clean up the context with the function UIGraphicsEndImageContext.

In BNRItem.m, implement the following methods to create a thumbnail using an offscreen context.

- (void)setThumbnailFromImage:(UIImage *)image

{

CGSize origImageSize = image.size;

// The rectangle of the thumbnail

CGRect newRect = CGRectMake(0, 0, 40, 40);

// Figure out a scaling ratio to make sure we maintain the same aspect ratio

float ratio = MAX(newRect.size.width / origImageSize.width,

newRect.size.height / origImageSize.height);

// Create a transparent bitmap context with a scaling factor

// equal to that of the screen

UIGraphicsBeginImageContextWithOptions(newRect.size, NO, 0.0);

// Create a path that is a rounded rectangle

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:newRect

cornerRadius:5.0];

// Make all subsequent drawing clip to this rounded rectangle

[path addClip];

// Center the image in the thumbnail rectangle

CGRect projectRect;

projectRect.size.width = ratio * origImageSize.width;

projectRect.size.height = ratio * origImageSize.height;

projectRect.origin.x = (newRect.size.width - projectRect.size.width) / 2.0;

projectRect.origin.y = (newRect.size.height - projectRect.size.height) / 2.0;

// Draw the image on it

[image drawInRect:projectRect];

// Get the image from the image context; keep it as our thumbnail

UIImage *smallImage = UIGraphicsGetImageFromCurrentImageContext();

self.thumbnail = smallImage;

// Cleanup image context resources; we're done

UIGraphicsEndImageContext();

}

In BNRDetailViewController.m, add the following line of code to imagePickerController:didFinishPickingMediaWithInfo: to create a thumbnail when the camera takes the original image.

- (void)imagePickerController:(UIImagePickerController *)picker

didFinishPickingMediaWithInfo:(NSDictionary *)info

{

UIImage *image = info[UIImagePickerControllerOriginalImage];

[self.item setThumbnailFromImage:image];

Now that instances of BNRItem have a thumbnail, you can use this thumbnail in BNRItemsViewController’s table view. In BNRItemsViewController.m, update tableView:cellForRowAtIndexPath:.

cell.valueLabel.text =

[NSString stringWithFormat:@"$%d", item.valueInDollars];

cell.thumbnailView.image = item.thumbnail;

return cell;

}

Now build and run the application. Take a picture for a BNRItem instance and return to the table view. That row will display a thumbnail image along with the name and value of the BNRItem. (Note that you will have to retake pictures for existing items.)

Do not forget to add the thumbnail data to your archive! Open BNRItem.m and make the following changes:

- (id)initWithCoder:(NSCoder *)aDecoder

{

self = [super init];

if (self) {

_itemName = [aDecoder decodeObjectForKey:@"itemName"];

_serialNumber = [aDecoder decodeObjectForKey:@"serialNumber"];

_dateCreated = [aDecoder decodeObjectForKey:@"dateCreated"];

_itemKey = [aDecoder decodeObjectForKey:@"itemKey"];

_thumbnail = [aDecoder decodeObjectForKey:@"thumbnail"];

_valueInDollars = [aDecoder decodeIntForKey:@"valueInDollars"]

}

return self;

}

- (void)encodeWithCoder:(NSCoder *)aCoder

{

[aCoder encodeObject:self.itemName forKey:@"itemName"];

[aCoder encodeObject:self.serialNumber forKey:@"serialNumber"];

[aCoder encodeObject:self.dateCreated forKey:@"dateCreated"];

[aCoder encodeObject:self.itemKey forKey:@"itemKey"];

[aCoder encodeObject:self.thumbnail forKey:@"thumbnail"];

[aCoder encodeInt:self.valueInDollars forKey:@"valueInDollars"];

}

Build and run the application. Take some photos of items and then exit and relaunch the application. The thumbnails will now appear for saved items.

Relaying Actions from UITableViewCells

Sometimes, it is useful to add a UIControl or one of its subclasses, like a UIButton, to a UITableViewCell. For instance, you want users to be able to tap the thumbnail image in a cell and see a full-size image for that item. In this section, you will do that by adding a transparent button on top of the thumbnail. Tapping this button will show the full-size image in a UIPopoverController when the application is running on an iPad.

Open BNRItemCell.m and stub out a method that will trigger showing the image.

- (IBAction)showImage:(id)sender

{

}

Now, in BNRItemCell.xib, drag a UIButton onto the content view. Remove the text from the button. Select both the UIImageView and UIButton, and then from the Align Auto Layout menu, select Leading Edges, Trailing Edges, Top Edges, and Bottom Edges. For the Update Frames drop-down, select Items of New Constraints, and then click Add 4 Constraints (Figure 19.11).

Figure 19.11 UIButton constraints

UIButton constraints

Finally, you need to connect the UIButton to the showImage: action. Open BNRItemCell.xib and BNRItemCell.m in the assistant editor, and Control-drag from the UIButton to the showImage: method (Figure 19.12).

Figure 19.12 UIButton constraints

UIButton constraints

So now you have a button that will send showImage: to BNRItemCell when it is tapped. Obviously, you will have to implement that method, but here you run into a problem: this message will be sent to the BNRItemCell, but BNRItemCell is not a controller and does not have access to any of the image data necessary to get the full-size image. In fact, it does not even have access to the BNRItem whose thumbnail it is displaying.

You might consider letting BNRItemCell keep a pointer to the BNRItem it displays. But table view cells are view objects, and they should not manage model objects or be able to present additional interfaces (like the UIPopoverController).

A better solution is to give BNRItemCell a block to execute when the button is tapped. This block will be supplied by the BNRItemsViewController that is responsible for configuring the cell.

Adding a block to the cell subclass

We briefly looked at blocks in Chapter 17, but let’s take a closer look now.

Open BNRItemCell.h and add a property for the block that will be executed.

@interface BNRItemCell : UITableViewCell

@property (nonatomic, weak) IBOutlet UIImageView *thumbnailView;

@property (nonatomic, weak) IBOutlet UILabel *nameLabel;

@property (nonatomic, weak) IBOutlet UILabel *serialNumberLabel;

@property (nonatomic, weak) IBOutlet UILabel *valueLabel;

@property (nonatomic, copy) void (^actionBlock)(void);

@end

The syntax may still be a bit scary, but recall that a block looks a lot like a function. Figure 19.13 shows the various components of a block.

Notice that the property is declared as copy. This is very important. Blocks behave a bit differently than the rest of the objects you have been working with so far. When a block is created, it is created on the stack, as opposed to being created on the heap like other objects. This means that when the method that a block is declared in is returned, any blocks that are created will be destroyed along with all of the other local variables. In order for a block to persist beyond the lifetime of the method it is declared in, a block must be sent the copy message. By doing so, the block will be copied to the heap – thus you declare the property to have the copy attribute.

Figure 19.13 Block syntax

Block syntax

In BNRItemCell.m, call the block when the button is tapped.

- (IBAction)showImage:(id)sender

{

if (self.actionBlock) {

self.actionBlock();

}

}

Note that you have to make sure that the block exists before calling it.

Let’s verify this all works according to plan. In BNRItemsViewController.m, update tableView:cellForRowAtIndexPath: to print out the index path.

- (UITableViewCell *)tableView:(UITableView *)tableView

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

BNRItem *item = [[BNRItemStore sharedStore] allItems][indexPath.row];

// Get the new or recycled cell

BNRItemCell *cell =

[tableView dequeueReusableCellWithIdentifier:@"BNRItemCell"

forIndexPath:indexPath];

// Configure the cell with the BNRItem

cell.nameLabel.text = item.itemName;

cell.serialNumberLabel.text = item.serialNumber;

cell.valueLabel.text = [NSString stringWithFormat:@"$%i", item.valueInDollars];

cell.thumbnailView.image = item.thumbnail;

cell.actionBlock = ^{

NSLog(@"Going to show image for %@", item);

};

return cell;

}

Build and run the application. Tap a thumbnail (or, more accurately, the transparent button on top of the thumbnail) and check the message in the console.

Presenting the image in a popover controller

Now, BNRItemsViewController needs to change the action block to grab the BNRItem associated with the cell whose button was tapped and display its image in a UIPopoverController.

To display an image in a popover, you need a new UIViewController whose view shows an image. Create a new Objective-C subclass. Name this new class BNRImageViewController, select UIViewController as its superclass, and uncheck all boxes.

Since this view controller will only have one view, you will create it programmatically. In BNRImageViewController.m, implement loadView.

- (void)loadView

{

UIImageView *imageView = [[UIImageView alloc] init];

imageView.contentMode = UIViewContentModeScaleAspectFit;

self.view = imageView;

}

Note that you do not need to set up any constraints because the UIPopoverController that the BNRImageViewController will appear in will always set the size of this image view to the size of the popover.

Now add a property to the public interface in BNRImageViewController.h to hold the image.

@interface BNRImageViewController : UIViewController

@property (nonatomic, strong) UIImage *image;

@end

When an instance of BNRImageViewController is created, it will be given an image. In BNRImageViewController.m, implement viewWillAppear: to set the view’s image from this image.

- (void)viewWillAppear:(BOOL)animated

{

[super viewWillAppear:animated];

// We must cast the view to UIImageView so the compiler knows it

// is okay to send it setImage:

UIImageView *imageView = (UIImageView *)self.view;

imageView.image = self.image;

}

Now you can finish implementing the action block. In BNRItemsViewController.m, add a property to hang on to a popover controller in the class extension and have this class conform to the UIPopoverControllerDelegate protocol.

@interface BNRItemsViewController () <UIPopoverControllerDelegate>

@property (nonatomic, strong) UIPopoverController *imagePopover;

@end

Next, import the appropriate header files at the top of BNRItemsViewController.m.

#import "BNRImageStore.h"

#import "BNRImageViewController.h"

Flesh out the implementation of the action block to present the popover controller that displays the full-size image for the BNRItem represented by the cell that was tapped.

cell.actionBlock = ^{

NSLog(@"Going to show image for %@", item);

if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {

NSString *itemKey = item.itemKey;

// If there is no image, we don't need to display anything

UIImage *img = [[BNRImageStore sharedStore] imageForKey:itemKey];

if (!img) {

return;

}

// Make a rectangle for the frame of the thumbnail relative to

// our table view

// Note: there will be a warning on this line that we'll soon discuss

CGRect rect = [self.view convertRect:cell.thumbnailView.bounds

fromView:cell.thumbnailView];

// Create a new BNRImageViewController and set its image

BNRImageViewController *ivc = [[BNRImageViewController alloc] init];

ivc.image = img;

// Present a 600x600 popover from the rect

self.imagePopover = [[UIPopoverController alloc]

initWithContentViewController:ivc];

self.imagePopover.delegate = self;

self.imagePopover.popoverContentSize = CGSizeMake(600, 600);

[self.imagePopover presentPopoverFromRect:rect

inView:self.view

permittedArrowDirections:UIPopoverArrowDirectionAny

animated:YES];

}

};

Finally, in BNRItemsViewController.m, get rid of the popover if the user taps anywhere outside of it .

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController

{

self.imagePopover = nil;

}

Build and run the application. Tap on the thumbnails in each row to see the full-size image in the popover. Tap anywhere else to dismiss the popover.

Variable Capturing

A block can use any variables that are visible within its enclosing scope. The enclosing scope of a block is the scope of the method in which it is defined. Thus, a block has access to all of the local variables of the method, arguments passed to the method, and instance variables that belong to the object running the method. In the actionBlock code above, both the BNRItem (item) and the BNRItemCell (cell) have been captured from the enclosing scope.

Blocks own the objects that they capture, and this can easily result in a strong reference cycle. Take a look back at the warning you had when creating rect within the actionBlock: “Capturing ‘cell’ strongly in this block is likely to lead to a strong reference cycle”. Since blocks own the objects they capture, this makes sense. The cell has ownership of actionBlock, and actionBlock has strong ownership of cell (Figure 19.14).

Figure 19.14 Cell and block own each other

Cell and block own each other

To fix this problem, actionBlock should have a weak reference to cell, which will break the strong reference cycle.

In BNRItemsViewController.m, update the actionBlock code to have a weak reference to the BNRItemCell.

__weak BNRItemCell *weakCell = cell;

cell.actionBlock = ^{

NSLog(@"Going to show image for %@", item);

BNRItemCell *strongCell = weakCell;

if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {

NSString *itemKey = item.itemKey;

// If there is no image, we don't need to display anything

UIImage *img = [[BNRImageStore sharedStore] imageForKey:itemKey];

if (!img) {

return;

}

// Make a rectangle that the frame of the thumbnail relative to

// our table view

// Note: there will be a warning on this line that we'll soon discuss

CGRect rect = [self.view convertRect:cell.thumbnailView.bounds

fromView:cell.thumbnailView];

CGRect rect = [self.view convertRect:strongCell.thumbnailView.bounds

fromView:strongCell.thumbnailView];

// Create a new BNRImageViewController and set its image

BNRImageViewController *ivc = [[BNRImageViewController alloc] init];

ivc.image = img;

// Present a 600x600 popover from the rect

self.imagePopover = [[UIPopoverController alloc]

initWithContentViewController:ivc];

self.imagePopover.delegate = self;

self.imagePopover.popoverContentSize = CGSizeMake(600, 600);

[self.imagePopover presentPopoverFromRect:rect

inView:self.view

permittedArrowDirections:UIPopoverArrowDirectionAny

animated:YES];

}

};

Once the block begins executing, you need to guarantee that the cell hangs around until it is done executing. For that reason, you temporarily take strong ownership of that cell by creating a strong reference to it with strongCell. Unlike taking a permanent strong reference to a variable from the enclosing scope, this way the block only has a strong reference to the cell object as long as the strongCell variable exists – that is, while the block is actually executing.

Build and run the application. The behavior will be the same, but your application no longer has a memory leak.

Bronze Challenge: Color Coding

If a BNRItem is worth more than $50, make its value label text appear in green. If it is worth less than $50, make it appear in red.

Gold Challenge: Zooming

The BNRImageViewController should center its image and allow zooming. Implement this behavior in BNRImageViewController.m.

For the More Curious: UICollectionView

The class UICollectionView is very similar to UITableView:

· It is a subclass of UIScrollView.

· It displays cells, although these cells inherit from UICollectionViewCell instead of UITableViewCell.

· It has a data source that supplies it with those cells.

· It has a delegate that gets informed about things like a cell being selected.

· Similar to UITableViewController, UICollectionViewController is a view controller class that creates a UICollectionView as its view and becomes its delegate and data source.

How is the collection view different? A table view only displays one column of cells; this is a huge limitation on a large-screen device like an iPad. A collection view can layout those cells any way you want. The most common layout is a grid (Figure 19.15).

Figure 19.15 Homepwner with a UICollectionView

Homepwner with a UICollectionView

How does the UICollectionView figure out how to arrange the cells? It has a layout object that controls the attributes of each cell, including its position and size. These layout objects inherit from the abstract class UICollectionViewLayout. If you are just laying your cells out in a grid, you can use an instance of UICollectionViewFlowLayout. If you are doing something fancy, you will need to create a custom subclass of UICollectionViewLayout.

Figure 19.16 Example object model for a UICollectionView

Example object model for a UICollectionView

The default UITableViewCell is quite usable. (You used it for several chapters before this one.) UICollectionViewCell is not. It has a content view, but the content view has no subviews. So, if you are creating a UICollectionView, you will need to create a subclass of UICollectionViewCell.

That is all you need to know to get your first collection view up and running. After that, you will want to play around with the background view, supplementary views (which are mostly used as headers and footers for sections), and decoration views. The cell also has its own background view and a selected background view (which is laid over the background view when the cell is selected).