I’m gonna ask you the three big questions. — Go ahead. — Who made you? — You did. — Who owns the biggest piece of you? — You do. — What would happen if I dropped you? — I’d go right down the drain.
A table view (UITableView) is a vertically scrolling UIScrollView (Chapter 7) containing a single column of rectangular cells (UITableViewCell, a UIView subclass). It is a keystone of Apple’s strategy for making the small iPhone screen useful and powerful, and has three main purposes:
In addition to its column of cells, a table view can be extended by a number of other features that make it even more useful and flexible:
Figure 8-1 illustrates four variations of the table view:
Table view cells, too, can be extremely flexible. Some basic cell formats are provided, such as a text label along with a small image view, but you are free to design your own cell as you would any other view. There are also some standard interface items that are commonly used in a cell, such as a checkmark to indicate selection or a right-pointing chevron to indicate that tapping the cell navigates to a detail view.
It would be difficult to overestimate the importance of table views. An iOS app without a table view somewhere in its interface would be a rare thing, especially on the small iPhone screen. I’ve written apps consisting almost entirely of table views. Indeed, it is not uncommon to use a table view even in situations that have nothing particularly table-like about them, simply because it is so convenient.
For example, in one of my apps I want the user to be able to choose between three levels of difficulty and two sets of images. In a desktop application I’d probably use radio buttons; but there are no radio buttons among the standard iOS interface objects. Instead, I use a grouped table view so small that it doesn’t even scroll. This gives me section headers, tappable cells, and a checkmark indicating the current choice (Figure 8-2).
There is a UIViewController subclass, UITableViewController, dedicated to the presentation of a table view. You never really need to use a UITableViewController; it’s a convenience, but it doesn’t do anything that you couldn’t do yourself by other means. Here’s some of what using a UITableViewController gives you:
initWithStyle:
creates the table view with a plain or grouped format.
tableView
. It is also, of course, the view controller’s view
, but the tableView
property is typed as a UITableView, so you can send table view messages to it without typecasting.
Beginners may be surprised to learn that a table view’s structure and contents are generally not configured in advance. Rather, you supply the table view with a data source and a delegate (which will often be the same object), and the table view turns to these in real time, as the app runs, whenever it needs a piece of information about its structure and contents.
This architecture is part of a brilliant strategy to conserve resources. Imagine a long table consisting of thousands of rows. It must appear, therefore, to consist of thousands of cells as the user scrolls. But a cell is a UIView and is memory-intensive; to maintain thousands of cells internally would put a terrible strain on memory. Therefore, the table typically maintains only as many cells as are showing simultaneously at any one moment (about ten, let’s say). As the user scrolls to reveal new cells, those cells are created on the spot; meanwhile, the cells that have been scrolled out of view are permitted to die.
This sounds ingenious but a bit wasteful, and possibly time-consuming. Wouldn’t it be even cleverer if, instead of letting a cell die as it is scrolled out of view, it were whisked around to the other side and used again as one of the cells being scrolled into view? Yes, and in fact that’s exactly what you’re supposed to do. You do it by assigning each cell a reuse identifier.
As cells with a given reuse identifier are scrolled out of view, the table view maintains a bunch of them in a pile. As cells are scrolled into view, you ask the table view for a cell from that pile, specifying it by means of the reuse identifier. The table view hands an old used cell back to you, and now you can configure it as the cell that is about to be scrolled into view. Cells are thus reused to minimize not only the number of actual cells in existence at any one moment, but the number of actual cells ever created. A table of 1000 rows might very well never need to create more than a dozen cells over the entire lifetime of the app.
Your code must be prepared, on demand, to supply the table with pieces of requested data. Of these, the most important is the cell to be slotted into a given position. A position in the table is specified by means of an index path (NSIndexPath), a class used here to combine a section number with a row number, and is often referred to simply as a row of the table. Your data source object may at any moment be sent the message tableView:cellForRowAtIndexPath:
, and must respond by returning the UITableViewCell to be displayed at that row of the table. And you must return it fast: the user is scrolling now, so the table needs the next cell now.
In this section, I’ll discuss what you’re going to be supplying — the table view cell. After that, I’ll talk about how you supply it.
The simplest way to obtain a table view cell is to start with one of the four built-in table view cell styles. To create a cell using a built-in style, call initWithStyle:reuseIdentifier:
. The reuseIdentifier:
is what allows cells previously assigned to rows that are no longer showing to be reused for cells that are; it will usually be the same for all cells in a table. Your choices of cell style are:
UITableViewCellStyleDefault
textLabel
), with an optional UIImageView (its imageView
) at the left. If there is no image, the label occupies the entire width of the cell.
UITableViewCellStyleValue1
textLabel
and its detailTextLabel
), side by side, with an optional UIImageView (its imageView
) at the left. The first label is left-aligned; the second label is right-aligned. If the first label’s text is too long, the second label won’t appear.
UITableViewCellStyleValue2
textLabel
and its detailTextLabel
), side by side. No UIImageView will appear. The first label is right-aligned; the second label is left-aligned. The label sizes are fixed, and the text of either will be truncated if it’s too long.
UITableViewCellStyleSubtitle
textLabel
and its detailTextLabel
), one above the other, with an optional UIImageView (its imageView
) at the left.
To experiment with the built-in cell styles, do this:
To get our table view into the interface, import "RootViewController.h"
into AppDelegate.m, and add this line to AppDelegate’s application:didFinishLaunchingWithOptions:
at the override point:
self.window.rootViewController = [RootViewController new];
Now modify the RootViewController class (which comes with a lot of templated code), as in Example 8-1. Run the app to see the world’s simplest table (Figure 8-3).
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 20; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; cell.textLabel.textColor = [UIColor redColor]; } cell.textLabel.text = [NSString stringWithFormat:@"Hello there! %d", indexPath.row]; return cell; }
The key parts of the code are:
Our table will have one section.
Our table will consist of 20 rows. Having multiple rows will give us a sense of how our cell looks when placed next to other cells.
This is where you specify the built-in table view cell style you want to experiment with.
At this point in the code you can modify characteristics of the cell (cell
) that are to be the same for every cell of the table. For the moment, I’ve symbolized this by assuming that every cell’s text is to be the same color.
We now have the cell to be used for this row of the table, so at this point in the code you can modify characteristics of the cell (cell
) that are unique to this row. I’ve symbolized this by appending successive numbers to the text of each row. Of course, that’s completely unrealistic; but that’s just because we’re only beginners. In real life the different cells would reflect meaningful data. I’ll talk about that later in this chapter.
Now you can experiment with your cell’s appearance by tweaking the code and running the app. Feel free to try different built-in cell styles in the place where we are now specifying UITableViewCellStyleDefault
.
The flexibility of each built-in style is based mostly on the flexibility of UILabels. Not everything can be customized, because after you return the cell some further configuration takes place, which may override your settings. For example, the size and position of the cell’s subviews are not up to you. (I’ll explain, a little later, how to get around that.) But you get a remarkable degree of freedom. Here are a few basic UILabel properties for you to play with now (by customizing cell.textLabel
), and I’ll talk much more about UILabels in Chapter 10:
text
textColor
, highlightedTextColor
The color of the text. The highlightedTextColor
applies when the cell is highlighted or selected (tap on a cell to select it).
In earlier versions of iOS, if you didn’t set the highlightedTextColor
, the label would choose its own variant of the textColor
when the cell was highlighted or selected. In iOS 7, that’s no longer the case; the textColor
is used unless you set the highlightedTextColor
explicitly.
textAlignment
NSTextAlignmentLeft
, NSTextAlignmentCenter
, and NSTextAlignmentRight
.
numberOfLines
0
means there’s no maximum.
font
The label’s font. You could reduce the font size as a way of fitting more text into the label. A font name includes its style. For example:
cell.textLabel.font = [UIFont fontWithName:@"Helvetica-Bold" size:12.0];
shadowColor
, shadowOffset
You can also assign the image view (cell.imageView
) an image. The frame of the image view can’t be changed, but you can inset its apparent size by supplying a smaller image and setting the image view’s contentMode
to UIViewContentModeCenter
. It’s probably a good idea in any case, for performance reasons, to supply images at their drawn size and resolution rather than making the drawing system scale them for you (see the last section of Chapter 7). For example:
CGFloat side = 30; UIImage* im = [UIImage imageNamed:@"smiley"]; UIGraphicsBeginImageContextWithOptions(CGSizeMake(side,side), YES, 0); [im drawInRect:CGRectMake(0,0,side,side)]; UIImage* im2 = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); cell.imageView.image = im2; cell.imageView.contentMode = UIViewContentModeCenter;
The cell itself also has some properties you can play with:
accessoryType
A built-in type of accessory view, which appears at the cell’s right end. For example:
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
accessoryView
Your own UIView, which appears at the cell’s right end (overriding the accessoryType
). For example:
UIButton* b = [UIButton buttonWithType:UIButtonTypeSystem]; [b setTitle:@"Tap Me" forState:UIControlStateNormal]; [b sizeToFit]; // ... also assign button a target and action ... cell.accessoryView = b;
indentationLevel
, indentationWidth
tableView:indentationLevelForRowAtIndexPath:
method.
separatorInset
15
, though if you don’t set it explicitly, the built-in table view cell styles may shift it. This property affects both the drawing of the separator between cells and the indentation of content of the built-in cell styles.
selectionStyle
How the background looks when the cell is selected. The default, new in iOS 7, is solid gray (UITableViewCellSelectionStyleDefault
), or you can choose UITableViewCellSelectionStyleNone
.
(The blue and gray gradient backgrounds designated by UITableViewCellSelectionStyleBlue
and UITableViewCellSelectionStyleGray
in iOS 6 and before are now abandoned, and are treated as equivalent to UITableViewCellSelectionStyleDefault
.)
backgroundColor
backgroundView
selectedBackgroundView
What’s behind everything else drawn in the cell. The selectedBackgroundView
is drawn in front of the backgroundView
(if any) when the cell is selected, and will appear instead of whatever the selectionStyle
dictates. The backgroundColor
is behind the backgroundView
. (Thus, if both the selectedBackgroundView
and the backgroundView
have some transparency, both of them and the backgroundColor
can appear composited together when the cell is selected.)
There is no need to set the frame of the backgroundView
and selectedBackgroundView
; they will be resized automatically to fit the cell.
multipleSelectionBackgroundView
allowsMultipleSelection
(or, if editing, allowsMultipleSelectionDuringEditing
) is YES, used instead of the selectedBackgroundView
when the cell is selected.
In this example, we set the cell’s backgroundView
to display an image with some transparency at the outside edges, so that the backgroundColor
shows behind it, and we set the selectedBackgroundView
to an almost transparent blue rectangle, to darken that image when the cell is selected (Figure 8-4):
cell.textLabel.textColor = [UIColor whiteColor]; UIImageView* v = [UIImageView new]; // no need to set frame v.contentMode = UIViewContentModeScaleToFill; v.image = [UIImage imageNamed:@"linen.png"]; cell.backgroundView = v; UIView* v2 = [UIView new]; // no need to set frame v2.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.2]; cell.selectedBackgroundView = v2; cell.backgroundColor = [UIColor redColor];
If those features are to be true of every cell ever displayed in the table, then that code should go in the spot numbered 4 in Example 8-1; there’s no need to waste time doing the same thing all over again when an existing cell is reused.
Finally, here are a few properties of the table view itself worth playing with:
rowHeight
tableView:heightForRowAtIndexPath:
method; thus a table’s cells may differ from one another in height (more about that later in this chapter).
separatorStyle
separatorColor
separatorInset
These can also be set in the nib. The table’s separatorInset
is adopted by individual cells that don’t have their own explicit separatorInset
. Separator styles are:
UITableViewCellSeparatorStyleNone
UITableViewCellSeparatorStyleSingleLine
.
(The former UITableViewCellSeparatorStyleSingleLineEtched
style is abandoned in iOS 7, and equates to None
.)
backgroundColor
, backgroundView
backgroundView
is drawn on top of the backgroundColor
.
tableHeaderView
, tableFooterView
Views to be shown before the first row and after the last row, respectively (as part of the table’s scrolling content). Their background color is, by default, the background color of the table, but you can change that. You dictate their heights; their widths will be dynamically resized to fit the table. The user can, if you like, interact with these views (and their subviews); for example, a view can be (or can contain) a UIButton.
You can alter a table header or footer view dynamically during the lifetime of the app; if you change its height, you must set the corresponding table view property afresh to notify the table view of what has happened.
In tableView:cellForRowAtIndexPath:
, there are actually two possible ways to obtain a reusable cell:
dequeueReusableCellWithIdentifier:
dequeueReusableCellWithIdentifier:forIndexPath:
If you use the second method, which was introduced in iOS 6, you pass along as the second argument the same indexPath:
value that you already received. I prefer the second method, and will use it from now on. It has three advantages:
dequeueReusableCellWithIdentifier:
, the value returned by dequeueReusableCellWithIdentifier:forIndexPath:
is never nil. If there is a free reusable cell with the given identifier, it is returned. If there isn’t, a new one is created for you. Step 3 of Example 8-1 can thus be eliminated.
dequeueReusableCellWithIdentifier:
, the cell returned by dequeueReusableCellWithIdentifier:forIndexPath:
has its final bounds. That’s possible because you’ve passed the index path as an argument, so the runtime knows this cell’s ultimate destination within the table, and has already consulted the table’s rowHeight
or the delegate’s tableView:heightForRowAtIndexPath:
. This makes laying out the cell’s contents much easier.
dequeueReusableCellWithIdentifier:
is that you may accidentally pass an incorrect reuse identifier, or nil, and end up not reusing cells. With dequeueReusableCellWithIdentifier:forIndexPath:
, that can’t happen.
Before you call dequeueReusableCellWithIdentifier:forIndexPath:
for the first time, you must register with the table itself. You do this by calling registerClass:forCellReuseIdentifier:
. This associates a class (which must be UITableViewCell or a subclass thereof) with a string identifier. That’s how dequeueReusableCellWithIdentifier:forIndexPath:
knows what class to instantiate when it creates a new cell for you: you pass an identifier, and you’ve already told the table what class it signifies. The only cell types you can obtain are those for which you’ve registered in this way; if you pass a bad identifier, the app will crash (with a helpful log message).
This is a very elegant mechanism. It also raises some new questions:
registerClass:forCellReuseIdentifier:
?
Call it early, before the table view starts generating cells. viewDidLoad
is a good place:
- (void)viewDidLoad { [super viewDidLoad]; [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"]; }
We are no longer calling initWithStyle:reuseIdentifier:
, so where do we make our choice of built-in cell style? The default cell style is UITableViewCellStyleDefault
, so if that’s what you wanted, the problem is solved. Otherwise, subclass UITableViewCell and override initWithStyle:reuseIdentifier:
to substitute the cell style you’re after (passing along the reuse identifier you were handed).
For example, let’s call our UITableViewCell subclass MyCell. So we now specify [MyCell class]
in our call to registerClass:forCellReuseIdentifier:
. MyCell’s initializer looks like this:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:UITableViewCellStyleSubtitle // or whatever reuseIdentifier:reuseIdentifier]; if (self) { // ... } return self; }
It’s important to know this, because there needs to be a way distinguish between step 4 of Example 8-1 (configurations to apply once and for all to a new cell) and step 5 (configurations that differ for each row). It’s up to you, when performing one-time configuration on a cell, to give that cell some distinguishing mark that you can look for later to determine whether a cell requires one-time configuration.
For example, if every cell is to have a green background, there is no point giving every cell returned by dequeueReusableCellWithIdentifier:forIndexPath:
a green background; the reused cells already have one. Now, however, no cell is nil, and we are never instantiating the cell ourselves. So how will we know which ones need to be given a green background? It’s easy: they are the ones without a green background:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; if (![cell.backgroundColor isEqual:[UIColor greenColor]]) { // do one-time configurations cell.textLabel.textColor = [UIColor redColor]; cell.backgroundColor = [UIColor greenColor]; } // do individual cell configurations cell.textLabel.text = [NSString stringWithFormat:@"Hello there! %d", indexPath.row]; return cell; }
Cell class registration with registerClass:forCellReuseIdentifier:
is also consistent with ways of generating a cell when you don’t want one of the four built-in table view cell styles, or when you want to design the cell with the nib editor, as I’ll now proceed to explain.
The built-in cell styles give the beginner a leg up in getting started with table views, but there is nothing sacred about them, and soon you’ll probably want to transcend them, putting yourself in charge of how a table’s cells look and what subviews they contain. The thing to remember is that the cell has a contentView
property, which is one of its subviews; things like the accessoryView
are outside the contentView
. All your customizations must be confined to subviews of the contentView
; this allows the cell to continue working correctly.
There are four possible approaches:
layoutSubviews
to alter the frames of the built-in subviews.
tableView:cellForRowAtIndexPath:
, add subviews to each cell’s contentView
as the cell is created. This approach can be combined with the previous one, or you can ignore the built-in subviews and use your own exclusively. As long as the built-in subviews for a particular built-in cell style are not referenced, they are never created or inserted into the cell, so you don’t need to remove them if you don’t want to use them.
tableView:cellForRowAtIndexPath:
each time a cell needs to be created.
I’ll illustrate each approach.
You can’t directly change the frame of a built-in cell style subview in tableView:cellForRowAtIndexPath:
, because after your changes, the cell’s layoutSubviews
comes along and overrides them. The workaround is to override the cell’s layoutSubviews
! This is a straightforward solution if your main objection to a built-in style is the frame of an existing subview.
To illustrate, let’s modify a UITableViewCellStyleDefault
cell so that the image is at the right end instead of the left end (Figure 8-5). We’ll make a UITableViewCell subclass, MyCell, remembering to register MyCell with the table view, so that dequeueReusableCellWithIdentifier:forIndexPath:
produces a MyCell instance; here is MyCell’s layoutSubviews
:
- (void) layoutSubviews { [super layoutSubviews]; CGRect cvb = self.contentView.bounds; CGRect imf = self.imageView.frame; imf.origin.x = cvb.size.width - imf.size.width - 15; self.imageView.frame = imf; CGRect tf = self.textLabel.frame; tf.origin.x = 15; self.textLabel.frame = tf; }
In using this technique, I find it easier to move the subviews using their frame, rather than with constraints. Otherwise, the runtime (which still thinks it owns these subviews) tries to fight us.
Instead of modifying the existing default subviews, you can add completely new views to each UITableViewCell’s content view. This has some great advantages over the preceding technique. We won’t be fighting the runtime, so we can make our changes in tableView:cellForRowAtIndexPath:
, and we can assign a frame or constraints. Here are some things to keep in mind:
addSubview:
to the cell itself — only to its contentView
(or some subview thereof).
autoresizingMask
or constraints, because the cell’s content view might be resized.
I’ll rewrite the previous example (Figure 8-5) to use this technique. We are no longer using a UITableViewCell subclass; the registered cell class is UITableViewCell itself. If this is a new cell, we add the subviews and assign them tags. If this is a reused cell, we don’t add the subviews (the cell already has them), and we use the tags to refer to the subviews:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; if (![cell viewWithTag:1]) { UIImageView* iv = [UIImageView new]; iv.tag = 1; [cell.contentView addSubview:iv]; UILabel* lab = [UILabel new]; lab.tag = 2; [cell.contentView addSubview:lab]; // position using constraints NSDictionary* d = NSDictionaryOfVariableBindings(iv, lab); iv.translatesAutoresizingMaskIntoConstraints = NO; lab.translatesAutoresizingMaskIntoConstraints = NO; // image view is vertically centered [cell.contentView addConstraint: [NSLayoutConstraint constraintWithItem:iv attribute:NSLayoutAttributeCenterY relatedBy:0 toItem:cell.contentView attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]]; // it's a square [cell.contentView addConstraint: [NSLayoutConstraint constraintWithItem:iv attribute:NSLayoutAttributeWidth relatedBy:0 toItem:iv attribute:NSLayoutAttributeHeight multiplier:1 constant:0]]; // label has height pinned to superview [cell.contentView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[lab]|" options:0 metrics:nil views:d]]; // horizontal margins [cell.contentView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-15-[lab]-15-[iv]-15-|" options:0 metrics:nil views:d]]; } UILabel* lab = (UILabel*)[cell.contentView viewWithTag: 2]; UIImageView* iv = (UIImageView*)[cell.contentView viewWithTag: 1]; // ... return cell; }
Using our own cell subviews instead of the built-in cell style subviews has some clear advantages; we no longer have to perform an elaborate dance to escape from the restrictions imposed by the runtime. Still, the verbosity of this code is somewhat overwhelming. We can avoid this by designing the cell in a nib.
In designing a cell in a nib, we start by creating a .xib file that will consist, in effect, solely of this one cell. In Xcode, create a new iOS User Interface View .xib file for iPhone. Let’s call it MyCell.xib. In the nib editor, delete the existing View and replace it with a Table View Cell from the Object library.
The cell’s design window shows a standard-sized cell; you can resize it as desired, but the actual size of the cell in the interface will be dictated by the table view’s width and its rowHeight
(or the delegate’s response to tableView:heightForRowAtIndexPath:
). The cell already has a contentView
, and any subviews you add will be inside that; do not subvert that arrangement.
You can choose a built-in table view cell style in the Style pop-up menu of the Attributes inspector, and this gives you the default subviews, locked in their standard positions; for example, if you choose Basic, the textLabel
appears, and if you specify an image, the imageView
appears. If you set the Style pop-up menu to Custom, you start with a blank slate. Let’s do that.
We’ll implement, from scratch, the same subviews we’ve already implemented in the preceding two examples: a UILabel on the left side of the cell, and a UIImageView on the right side. Just as when adding subviews in code, we should set each subview’s autoresizing behavior or constraints, and give each subview a tag, so that later, in tableView:cellForRowAtIndexPath:
, we’ll be able to refer to the label and the image view using viewWithTag:
, exactly as in the previous example:
UILabel* lab = (UILabel*)[cell viewWithTag: 2]; UIImageView* iv = (UIImageView*)[cell viewWithTag: 1]; // ... return cell;
The only remaining question is how to load the cell from the nib. This the Really Cool Part. When we register with the table view, which we’re currently doing in viewDidLoad
, instead of calling registerClass:forCellReuseIdentifier:
, we call registerNib:forCellReuseIdentifier:
. To specify the nib, call UINib’s class method nibWithNibName:bundle:
, like this:
[self.tableView registerNib:[UINib nibWithNibName:@"MyCell" bundle:nil] forCellReuseIdentifier:@"Cell"];
That’s all there is to it! In tableView:cellForRowAtIndexPath:
, when we call dequeueReusableCellWithIdentifier:forIndexPath:
, if the table has no free reusable cell already in existence, the nib will automatically be loaded and the cell will be instantiated from it and returned to us.
You may wonder how that’s possible, when we haven’t specified a File’s Owner class or added an outlet from the File’s Owner to the cell in the nib. The answer is that the nib conforms to a specific format. The UINib instance method instantiateWithOwner:options:
can load a nib with a nil owner; regardless, it returns an NSArray of the nib’s instantiated top-level objects. A nib registered with the table view is expected to have exactly one top-level object, and that top-level object is expected to be a UITableViewCell; that being so, the cell can easily be extracted from the resulting NSArray, as it is the array’s only element. Our nib meets those expectations!
The advantages of this approach should be immediately obvious. The subviews can now be designed in the nib, and code that was creating and configuring each subview can be deleted. For example, suppose we previously had this code:
if (![cell viewWithTag:1]) { UIImageView* iv = [UIImageView new]; iv.tag = 1; [cell.contentView addSubview:iv]; UILabel* lab = [UILabel new]; lab.tag = 2; [cell.contentView addSubview:lab]; // ... lab.font = [UIFont fontWithName:@"Helvetica-Bold" size:16]; lab.lineBreakMode = NSLineBreakByWordWrapping; lab.numberOfLines = 2; }
All of that can now be eliminated, including setting the label’s font
, lineBreakMode
, and numberOfLines
; those configurations are to be applied to the label in every cell, so they can be performed in the nib instead.
The nib must conform to the format I’ve described: it must have exactly one top-level object, a UITableViewCell. This means that some configurations are difficult or impossible in the nib. Consider, for example, the cell’s backgroundView
. The cell in the nib has a backgroundView
outlet, but if we drag a view into the canvas and hook that outlet to it, our app will crash when the nib loads (because there are now two top-level nib objects). The simplest workaround is to add the backgroundView
in code.
In tableView:cellForRowAtIndexPath:
, we are still referring to the cell’s subviews by way of viewWithTag:
. There’s nothing wrong with that, but perhaps you’d prefer to use names. Now that we’re designing the cell in a nib, that’s easy. Provide a UITableViewCell subclass with outlet properties, and configure the nib file accordingly:
Create the files for a UITableViewCell subclass; let’s call it MyCell. In MyCell.h, declare two outlet properties:
@property (nonatomic, weak) IBOutlet UILabel* theLabel; @property (nonatomic, weak) IBOutlet UIImageView* theImageView;
The result is that in our implementation of tableView:cellForRowAtIndexPath:
, once we’ve typed the cell as a MyCell (which will require importing "MyCell.h"
), the compiler will let us use the property names to access the subviews:
MyCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; UILabel* lab = cell.theLabel; UIImageView* iv = cell.theImageView;
When your table view comes from a storyboard, it is open to you to employ any of the ways of obtaining and designing its cells that I’ve already described. There is also an additional option, available only if you’re using a UITableViewController subclass — that is, there needs to be a UITableViewController scene in the storyboard. In that case, you can have the table view obtain the cells from the storyboard itself, and you can also design the cell directly in the table view in the storyboard.
To experiment with this way of obtaining and designing a cell, start with an iPhone project based on the Single View Application template:
The table view now contains a single table view cell with a content view. You can do in this cell exactly what we were doing before when designing a table view cell in a .xib file.
So, let’s do that. I like being able to refer to my custom cell subviews with property names. Our procedure is just like what we did in the previous example:
In the project, create the files for a UITableViewCell subclass; let’s call it MyCell. In MyCell.h, declare two outlet properties:
@property (nonatomic, weak) IBOutlet UILabel* theLabel; @property (nonatomic, weak) IBOutlet UIImageView* theImageView;
There is one question I have not yet answered: How will your code tell the table view to get its cells from the storyboard? Clearly, not by calling registerClass:forCellReuseIdentifier:
, and not by calling registerNib:forCellReuseIdentifier:
; each of those would do something perfectly valid, but not the thing we want done in this case. The answer is that you don’t register anything with the table view at all! Instead, when you call dequeueReusableCellWithIdentifier:forIndexPath:
, you supply an identifier that matches the prototype cell’s identifier in the storyboard.
So, return once more to the storyboard:
Cell
in the Identifier field. (This is an NSString, so capitalization counts.)
Now RootViewController’s tableView:cellForRowAtIndexPath:
works exactly as it did in the previous example:
MyCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; UILabel* lab = cell.theLabel; UIImageView* iv = cell.theImageView;
If you call dequeueReusableCellWithIdentifier:forIndexPath:
with an identifier that you have not registered with the table view and that doesn’t match the identifier of a prototype cell in the storyboard, your app will crash (with a helpful message in the console).
The structure and content of the actual data portrayed in a table view comes from the data source, an object pointed to by the table view’s dataSource
property and adopting the UITableViewDataSource protocol. The data source is thus the heart and soul of the table. What surprises beginners is that the data source operates not by setting the table view’s structure and content, but by responding on demand. The data source, qua data source, consists of a set of methods that the table view will call when it needs information. This architecture has important consequences for how you write your code, which can be summarized by these simple guidelines:
This may sound daunting, but you’ll be fine as long as you maintain an unswerving adherence to the principles of model–view–controller. How and when you accumulate the actual data, and how that data is structured, is a model concern. Acting as a data source is a controller concern. So you can acquire and arrange your data whenever and however you like, just so long as when the table view actually turns to you and asks what to do, you can lay your hands on the relevant data rapidly and consistently. You’ll want to design the model in such a way that the controller can access any desired piece of data more or less instantly.
Another source of confusion for beginners is that methods are rather oddly distributed between the data source and the delegate, an object pointed to by the table view’s delegate
property and adopting the UITableViewDelegate protocol; in some cases, one may seem to be doing the job of the other. This is not usually a cause of any real difficulty, because the object serving as data source will probably also be the object serving as delegate. Nevertheless, it is rather inconvenient when you’re consulting the documentation; you’ll probably want to keep the data source and delegate documentation pages open simultaneously as you work.
If a table view’s contents are known beforehand, you can alternatively design the entire table, including the contents of individual cells, in a storyboard. I’ll give an example later in this chapter.
Like Katherine Hepburn in Pat and Mike, the basis of your success (as a data source) is your ability, at any time, to answer the Three Big Questions. The questions the table view will ask you are a little different from the questions Mike asks Pat, but the principle is the same: know the answers, and be able to recite them at any moment. Here they are:
numberOfSectionsInTableView:
; respond with an integer. In theory you can sometimes omit this method, as the default response is 1
, which is often correct. However, I never omit it; for one thing, returning 0
is a good way to say that the table has no data, and will prevent the table view from asking any other questions.
tableView:numberOfRowsInSection:
. The table supplies a section number — the first section is numbered 0
— and you respond with an integer. In a table with only one section, of course, there is probably no need to examine the incoming section number.
tableView:cellForRowAtIndexPath:
. The index path is expressed as an NSIndexPath; this is a sophisticated and powerful class, but you don’t actually have to know anything about it, because UITableView provides a category on it that adds two read-only properties — section
and row
. Using these, you extract the requested section number and row number, and return a fully configured UITableViewCell, ready for display in the table view. The first row of a section is numbered 0
. I have already explained how to obtain the cell in the first place, by calling tableView:dequeueReusableCellWithIdentifier:
.
I have nothing particular to say about precisely how you’re going to fulfill these obligations. It all depends on your data model and what your table is trying to portray. The important thing is to remember that you’re going to be receiving an NSIndexPath specifying a section and a row, and you need to be able to lay your hands on the data corresponding to that slot now and configure the cell now. So construct your model, and your algorithm for consulting it in the Three Big Questions, and your way of configuring the cell, in accordance with that necessity.
For example, suppose our table is to list the names of the Pep Boys. Our data model might be an NSArray of string names (self.pep
). Our table has only one section. So our code might look like this:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { if (!self.pep) // data not ready? return 0; return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.pep count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; cell.theLabel.text = (self.pep)[indexPath.row]; return cell; }
At this point you may be feeling some exasperation. You want to object: “But that’s trivial!” Exactly so! Your access to the data model should be trivial. That’s the sign of a data model that’s well designed for access by your table view’s data source. Your implementation of tableView:cellForRowAtIndexPath:
might have some interesting work to do in order to configure the form of the cell, but accessing the actual data should be simple and boring.
For example, consider Figure 6-1. The actual code that fetches the data is trivial:
FPItem* item = self.parsedData.items[indexPath.row]; NSString* title = item.title; NSString* blurb = item.blurbOfItem;
That’s all there is to it. And the reason why that’s all there is to it is that I’ve structured the data model to be ready for access in exactly this way. (To be sure, there then follow about thirty lines of code elaborately — but very quickly — formatting the layout of the text within the cell.)
Another important goal of tableView:cellForRowAtIndexPath:
should be to conserve resources by reusing cells. As I’ve already explained, once a cell’s row is no longer visible on the screen, that cell can be slotted into a row that is visible — with its portrayed data appropriately modified, of course! — so that only a few more than the number of simultaneously visible cells will ever need to be instantiated.
A table view is ready to implement this strategy for you; all you have to do is call dequeueReusableCellWithIdentifier:forIndexPath:
. For any given identifier, you’ll be handed either a newly minted cell or a reused cell that previously appeared in the table view but is now no longer needed because it has scrolled out of view.
The table view can maintain more than one cache of reusable cells; this could be useful if your table view contains more than one type of cell (where the meaning of the concept “type of cell” is pretty much up to you). This is why you must name each cache, by attaching an identifier string to any cell that can be reused. All the examples in this chapter (and in this book, and in fact in every UITableView I’ve ever created) use just one cache and just one identifier, but there can be more than one. If you’re using a storyboard as a source of cells, there would then need to be more than one prototype cell.
To prove to yourself the efficiency of the cell-caching architecture, do something to differentiate newly instantiated cells from reused cells, and count the newly instantiated cells, like this:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 1000; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; UILabel* lab = cell.theLabel; lab.text = [NSString stringWithFormat:@"This is row %d of section %d", indexPath.row, indexPath.section]; if (lab.tag != 999) { lab.tag = 999; NSLog(@"%@", @"New cell"); } return cell; }
When we run this code and scroll through the table, every cell is numbered correctly, so there appear to be 1000 cells. But the log messages show that only about a dozen distinct cells are ever actually created.
Be certain that your table view code passes that test, and that you are truly reusing cells! Fortunately, one of the benefits of calling dequeueReusableCellWithIdentifier:forIndexPath:
is that it forces you to use a valid reuse identifier. Still, I’ve seen beginners obtain a cell in some other way, or even call dequeueReusableCellWithIdentifier:forIndexPath:
without understanding what it really does, only to instantiate a fresh cell manually in the next line. Don’t do that.
When your tableView:cellForRowAtIndexPath:
implementation configures individual cells (stage 5 in Example 8-1), the cell might be new or reused; at this point in your code, you don’t know or care which. Therefore, you should always configure everything about the cell that might need configuring. If you fail to do this, and if the cell is reused, you might be surprised when some aspect of the cell is left over from its previous use; similarly, if you fail to do this, and if the cell is new, you might be surprised when some aspect of the cell isn’t configured at all.
As usual, I learned that lesson the hard way. In the TidBITS News app, there is a little loudspeaker icon that should appear in a given cell in the master view’s table view only if there is a recording associated with this article. So I initially wrote this code:
if (item.enclosures && [item.enclosures count]) cell.speaker.hidden = NO;
That turned out to be a mistake, because the cell might be reused, and therefore always had a visible loudspeaker icon if, in a previous usage, that cell had ever had a visible loudspeaker icon! The solution was to rewrite the logic to cover all possibilities completely, like this:
cell.speaker.hidden = !(item.enclosures && [item.enclosures count]);
You do get a sort of second bite of the cherry: there’s a delegate method, tableView:willDisplayCell:forRowAtIndexPath:
, that is called for every cell just before it appears in the table. This is absolutely the last minute to configure a cell. But don’t misuse this method. You’re functioning as the delegate here, not the data source; you may set the final details of the cell’s appearance, but you shouldn’t be consulting the data model at this point.
An additional delegate method (introduced in iOS 6) is tableView:didEndDisplayingCell:forRowAtIndexPath:
. This tells you that the cell no longer appears in the interface and has become free for reuse. You could take advantage of this to tear down any resource-heavy customization of the cell — I’ll give an example in Chapter 24 — or simply to prepare it somehow for subsequent future reuse.
Your table data can be expressed as divided into sections. You might clump your data into sections for various reasons (and doubtless there are other reasons beyond these):
Don’t confuse the section headers and footers with the header and footer of the table as a whole. The latter are view properties of the table view itself and are set through its properties tableHeaderView
and tableFooterView
, discussed earlier in this chapter. The table header appears only when the table is scrolled all the way down; the table footer appears only when the table is scrolled all the way up.
A section header or footer, on the other hand, appears before or after its section, but (in a nongrouped table) it also detaches itself while the user scrolls the table, positioning itself at the top or bottom of the table view and floating over the scrolled rows, so that the user always knows what section is currently being viewed.
The number of sections is determined by your reply to numberOfSectionsInTableView:
. For each section, the table view will consult your data source and delegate to learn whether this section has a header or a footer, or both, or neither (the default).
The UITableViewHeaderFooterView class (introduced in iOS 6) is a UIView subclass intended specifically for use as the view of a header or footer; much like a table view cell, it is reusable. It has the following properties:
textLabel
detailTextLabel
contentView
textLabel
; the textLabel
is not inside the contentView
and in a sense doesn’t belong to you.
backgroundView
contentView
is in front of the backgroundView
. The contentView
has a clear background by default, so the backgroundView
shows through. An opaque contentView.backgroundColor
, on the other hand, would completely obscure the backgroundView
. In iOS 7, the header or footer view has a default backgroundView
whose backgroundColor
is derived (in some annoyingly unspecified way) from the table’s backgroundColor
.
backgroundColor
backgroundColor
; instead, set the backgroundColor
of its contentView
, or assign a new backgroundView
and configure it as you like.
tintColor
tintColor
affected the color of the background of the view. It also feels like a bug; the tintColor
should now affect the color of subviews, such a UIButton’s title, but, as of this writing, it doesn’t.)
You can supply a header or footer in two ways:
tableView:titleForHeaderInSection:
or tableView:titleForFooterInSection:
(or both). Return nil to indicate that the given section has no header (or footer). The header or footer view itself is a UITableViewHeaderFooterView, and is reused automatically: there will be only as many as needed for simultaneous display on the screen. The string you supply becomes the view’s textLabel.text
; however, in a grouped style table the string’s capitalization may be changed. To avoid that, use the second way of supplying the header or footer.
You implement the delegate method tableView:viewForHeaderInSection:
or tableView:viewForFooterInSection:
(or both). The view you supply is used as the entire header or footer and is automatically resized to the table’s width and the section header or footer height (I’ll discuss how the height is determined in a moment).
You are not required to return a UITableViewHeaderFooterView, but you will probably want to, in order to take advantage of reusability. To do so, the procedure is much like making a cell reusable. You register beforehand with the table view by calling registerClass:forHeaderFooterViewReuseIdentifier:
. To supply the reusable view, send the table view dequeueReusableHeaderFooterViewWithIdentifier:
; the result will be either a newly instantiated view or a reused view.
You can then configure this view as desired. For example, you can set its textLabel.text
, or you can give its contentView
custom subviews. In the latter case, be sure to set proper autoresizing or constraints, so that the subviews will be positioned and sized appropriately when the view itself is resized.
The documentation lists a second way of registering a header or footer view for reuse — registerNib:forHeaderFooterViewReuseIdentifier:
. But the nib editor’s Object library doesn’t include a UITableViewHeaderFooterView! This makes registerNib:forHeaderFooterViewReuseIdentifier:
pretty much useless, because there’s no way to configure the view correctly in the nib.
In addition, two pairs of delegate methods permit you to perform final configurations on your header or footer views:
tableView:willDisplayHeaderView:forSection:
tableView:willDisplayFooterView:forSection:
titleFor...
and then tweak its form slightly here. These delegate methods are matched by tableView:didEndDisplayingHeaderView:forSection:
and tableView:didEndDisplayingFooterView:forSection:
.
tableView:heightForHeaderInSection:
tableView:heightForFooterInSection:
sectionHeaderHeight
and sectionFooterHeight
unless you implement one of these methods to say otherwise.
It is possible to implement both viewFor...
and titleFor...
. In that case, viewFor...
is called first, and if it returns a UITableViewHeaderFooterView, titleFor...
will set its textLabel.text
.
If you implement both methods and you want heightFor...
to return the height as set by the table view based on titleFor...
, return UITableViewAutomaticDimension
.
Some lovely effects can be created by making use of the fact that a header or footer view will be further forward than the table’s cells. For example, a header with transparency shows the cells as they scroll behind it; a header with a shadow casts that shadow on the adjacent cell.
Now let’s talk about where the header or footer view’s data will come from. Clearly, a table that is to have section headers or footers (or both) may require some advance planning in the formation of its data model. Just as with a cell, a section title must be readily available so that it can be supplied quickly in real time. A structure that I commonly use is a pair of parallel arrays: an array of strings containing the section names, and an array of subarrays containing the data for each section.
For example, suppose we intend to display the names of all 50 U.S. states in alphabetical order as the rows of a table view, and that we wish to divide the table into sections according to the first letter of each state’s name. I’ll prepare the data model by walking through the alphabetized list of state names, creating a new section name and a new subarray when I encounter a new first letter:
NSString* s = [NSString stringWithContentsOfFile: [[NSBundle mainBundle] pathForResource:@"states" ofType:@"txt"] encoding:NSUTF8StringEncoding error:nil]; NSArray* states = [s componentsSeparatedByString:@"\n"]; self.sectionNames = [NSMutableArray array]; self.sectionData = [NSMutableArray array]; NSString* previous = @""; for (NSString* aState in states) { // get the first letter NSString* c = [aState substringToIndex:1]; // only add a letter to sectionNames when it's a different letter if (![c isEqualToString: previous]) { previous = c; [self.sectionNames addObject: [c uppercaseString]]; // and in that case, also add a new subarray to our array of subarrays NSMutableArray* oneSection = [NSMutableArray array]; [self.sectionData addObject: oneSection]; } [[self.sectionData lastObject] addObject: aState]; }
The value of this preparatory dance is evident when we are bombarded with questions from the table view about cells and headers; supplying the answers is trivial:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.sectionNames count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [(self.sectionData)[section] count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; NSString* s = self.sectionData[indexPath.section][indexPath.row]; cell.textLabel.text = s; return cell; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return self.sectionNames[section]; }
Let’s modify that example to illustrate customization of a header view. I’ve already registered my header identifier in viewDidLoad
:
[self.tableView registerClass:[UITableViewHeaderFooterView class] forHeaderFooterViewReuseIdentifier:@"Header"];
Now, instead of tableView:titleForHeaderInSection:
, I’ll implement tableView:viewForHeaderInSection:
. For completely new views, I’ll place my own label and an image view inside the contentView
and give them their basic configuration; then I’ll perform individual configuration on all views, new or reused (very much like tableView:cellForRowAtIndexPath:
):
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { UITableViewHeaderFooterView* h = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"Header"]; if (![h.tintColor isEqual: [UIColor redColor]]) { h.tintColor = [UIColor redColor]; h.backgroundView = [UIView new]; h.backgroundView.backgroundColor = [UIColor blackColor]; UILabel* lab = [UILabel new]; lab.tag = 1; lab.font = [UIFont fontWithName:@"Georgia-Bold" size:22]; lab.textColor = [UIColor greenColor]; lab.backgroundColor = [UIColor clearColor]; [h.contentView addSubview:lab]; UIImageView* v = [UIImageView new]; v.tag = 2; v.backgroundColor = [UIColor blackColor]; v.image = [UIImage imageNamed:@"us_flag_small.gif"]; [h.contentView addSubview:v]; lab.translatesAutoresizingMaskIntoConstraints = NO; v.translatesAutoresizingMaskIntoConstraints = NO; [h.contentView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-5-[lab(25)]-10-[v(40)]" options:0 metrics:nil views:@{@"v":v, @"lab":lab}]]; [h.contentView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[v]|" options:0 metrics:nil views:@{@"v":v}]]; [h.contentView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[lab]|" options:0 metrics:nil views:@{@"lab":lab}]]; } UILabel* lab = (UILabel*)[h.contentView viewWithTag:1]; lab.text = self.sectionNames[section]; return h; }
If your table view has the plain style, you can add an index down the right side of the table, where the user can tap or drag to jump to the start of a section — helpful for navigating long tables. To generate the index, implement the data source method sectionIndexTitlesForTableView
:, returning an NSArray of string titles to appear as entries in the index. This works even if there are no section headers. The index will appear only if the number of rows exceeds the table view’s sectionIndexMinimumDisplayRowCount
property value; the default is 0
, so the index is always displayed by default. You will want the index entries to be short — preferably just one character — because they will be partially obscuring the right edge of the table; plus, each cell’s content view will shrink to compensate, so you’re sacrificing some cell real estate.
For our list of state names, that’s trivial, as it should be:
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { return self.sectionNames; }
You can modify three properties that affect the index’s appearance:
sectionIndexColor
sectionIndexBackgroundColor
clearColor
, because otherwise the index distorts the colors of what’s behind it in a distracting way.
sectionIndexTrackingBackgroundColor
sectionIndexBackgroundColor
.
Normally, there will be a one-to-one correspondence between the index entries and the sections; when the user taps an index entry, the table jumps to the start of the corresponding section. However, under certain circumstances you may want to customize this correspondence.
For example, suppose there are 100 sections, but there isn’t room to display 100 index entries comfortably on the iPhone. The index will automatically curtail itself, omitting some index entries and inserting bullets to suggest the omission, but you might prefer to take charge of the situation.
To do so, supply a shorter index, and implement the data source method tableView:sectionForSectionIndexTitle:atIndex:
, returning the number of the section to jump to. You are told both the title and the index number of the section index listing that the user chose, so you can use whichever is convenient.
Apple’s documentation elaborates heavily on the details of implementing the model behind a table with an index and suggests that you rely on a class called UILocalizedIndexedCollation. This class is effectively a way of generating an ordered list of letters of the alphabet, with methods for helping to sort an array of strings and separate it into sections. This might be useful if you need your app to be localized, because the notion of the alphabet and its order changes automatically depending on the user’s preferred language.
Unfortunately, you can’t readily use a UILocalizedIndexCollation to implement your own sort order. For example, UILocalizedIndexCollation was of no use to me in writing my Greek and Latin vocabulary apps, in which the Greek words must be sorted, sectioned, and indexed according to the Greek alphabet, and the Latin words use a reduced version of the English alphabet (no initial J, K, or V through Z). Thus I’ve never actually bothered to use UILocalizedIndexedCollation.
The table view has no direct connection to the underlying data. If you want the table view display to change because the underlying data have changed, you have to cause the table view to refresh itself; basically, you’re requesting that the Big Questions be asked all over again. At first blush, this seems inefficient (“regenerate all the data??”); but it isn’t. Remember, in a table that caches reusable cells, there are no cells of interest other than those actually showing in the table at this moment. Thus, having worked out the layout of the table through the section header and footer heights and row heights, the table has to regenerate only those cells that are actually visible.
You can cause the table data to be refreshed using any of several methods:
reloadData
reloadRowsAtIndexPaths:withRowAnimation:
indexPathForRow:inSection:
.
reloadSections:withRowAnimation:
The second two methods can perform animations that cue the user as to what’s changing. For the rowAnimation:
argument, you’ll pass one of the following:
UITableViewRowAnimationFade
UITableViewRowAnimationRight
, UITableViewRowAnimationLeft
UITableViewRowAnimationTop
, UITableViewRowAnimationBottom
UITableViewRowAnimationNone
UITableViewRowAnimationMiddle
UITableViewRowAnimationAutomatic
If all you need to do is to refresh the index, call reloadSectionIndexTitles
; this calls the data source’s sectionIndexTitlesForTableView:
.
If you want the table view to be laid out freshly without reloading any cells, send it beginUpdates
immediately followed by endUpdates
. The section and row structure of the table will be asked for, along with calculation of all heights, but no cells and no headers or footers are requested. This is useful as a way of alerting the table that its measurements have changed. It might be considered a misuse of an updates block (the real use of such a block is discussed later in this chapter); but Apple takes advantage of this trick in the Table View Animations and Gestures example, in which a pinch gesture is used to change a table’s row height in real time, so it must be legal.
It is also possible to access and alter a table’s individual cells directly. This can be a lightweight approach to refreshing the table, plus you can supply your own animation within the cell as it alters its appearance. It is important to bear in mind, however, that the cells are not the data (view is not model). If you change the content of a cell manually, make sure that you have also changed the model corresponding to it, so that the row will appear correctly if its data is reloaded later.
To do this, you need direct access to the cell you want to change. You’ll probably want to make sure the cell is visible within the table view’s bounds; nonvisible cells don’t really exist (except as potential cells waiting in the reuse cache), and there’s no point changing them manually, as they’ll be changed when they are scrolled into view, through the usual call to tableView:cellForRowAtIndexPath:
.
Here are some UITableView methods that mediate between cells, rows, and visibility:
visibleCells
indexPathsForVisibleRows
cellForRowAtIndexPath:
indexPathForCell:
By the same token, you can get access to the views constituting headers and footers, by calling headerViewForSection:
or footerViewForSection:
. Thus you could modify a view directly. You should assume that if a section is returned by indexPathsForVisibleRows
, its header or footer might be visible.
If you want to grant the user some interface for requesting that a table view be refreshed, you might like to use a UIRefreshControl. You aren’t required to use this; it’s just Apple’s attempt to provide a standard interface. In iOS 7, it is located behind the top of the scrolling part of the table view, behind the table view’s backgroundView
. To request a refresh, the user scrolls the table view downward to reveal the refresh control and holds long enough to indicate that this scrolling is deliberate. The refresh control then acknowledges visually that it is refreshing, and remains visible until refreshing is complete.
To give a table view a refresh control, assign a UIRefreshControl to the table view controller’s refreshControl
property; it is a control (UIControl, Chapter 12), and you will want to hook its Value Changed event to an action method:
self.refreshControl = [UIRefreshControl new]; [self.refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];
You can also configure a table view controller’s refresh control in the nib editor.
Once a refresh control’s action message has fired, the control remains visible and indicates by animation (similar to an activity indicator) that it is refreshing, until you send it the endRefreshing
message:
-(void)doRefresh:(UIRefreshControl*) sender { // ... refresh the table data ... [sender endRefreshing]; }
You can initiate a refresh animation in code with beginRefreshing
, but this does not fire the action message or display the refresh control; to display it, scroll the table view:
[self.tableView setContentOffset: CGPointMake(0,-self.refreshControl.bounds.size.height) animated:YES]; [self.refreshControl beginRefreshing]; // ... now actually do refresh, and later send endRefreshing
A refresh control also has these properties:
refreshing
(read-only)
tintColor
attributedTitle
backgroundColor
(inherited from UIView)
refreshControl
a background color, that color completely covers the table view’s own background color when the refresh control is revealed. For some reason, I find the drawing of the attributedTitle
more reliable if the refresh control has a background color.
Most tables have rows that are all the same height, as set by the table view’s rowHeight
. However, the delegate’s tableView:heightForRowAtIndexPath:
can be used to make different rows different heights. You can see an example in the TidBITS News app (Figure 6-1).
Implementing variable row heights can be a tricky business. One problem is that as a cell is reused or instantiated from a nib, its height may be changed, which is likely to expose any weaknesses in your practice for laying out subviews. (Can you guess that I’m speaking from experience here?) A mistake in the autoresizingMask
value of subviews can result in display errors that would not have arisen if all the rows were the same height. You may have to resort to manual layout (such as implementing layoutSubviews
in a UITableViewCell subclass); alternatively, constraints can be a big help.
Another problem has to do with time consumed and the order of events. The runtime needs to know the heights of everything in your table immediately, before it starts asking for any cells. In effect, this means you have to gather all the data and lay out all the cells before you can start showing the data in any single row. If that takes a long time, your table view will remain blank during the calculation. Plus, there is now a danger of duplicating your own work, since you’ll be laying out every cell twice, once when you’re asked for all the heights and again when you’re asked for the actual cell.
In iOS 7, this second problem is not as severe as it used to be, thanks to three new table view properties:
estimatedRowHeight
estimatedSectionHeaderHeight
estimatedSectionFooterHeight
To accompany those, there are also three new table view delegate methods:
tableView:estimatedHeightForRowAtIndexPath:
tableView:estimatedHeightForHeaderInSection:
tableView:estimatedHeightForFooterInSection:
The idea is that if you implement these, the runtime will use their values whenever it gathers the heights for the whole table at once. Then, when a cell or a header or footer is about to appear onscreen, the runtime will ask for its real height in the usual way. You can thus get the table to appear quickly and postpone the calculation of an actual height until it is needed (by which time you might have calculated all the real heights in a background thread).
To illustrate, I’ll describe the strategy I use in the TidBITS News app. It predates the estimated
heights, so I don’t use them. The key is an NSMutableArray property, self.rowHeights
, consisting of NSNumbers wrapping floats. (An array is all that’s needed, because the table has just one section; the row number can thus serve directly as an index into the array.) Once that array is constructed, it can be used to supply a requested height instantly.
As I’ve already said, calculating a cell height requires me to lay out that cell. Each cell displays an attributed string. I have a utility method, attributedStringForIndexPath:
, which generates the attributed string for a given row of the table (by consulting the data model to obtain the title and description of the story represented by that row).
When the delegate’s tableView:heightForRowAtIndexPath:
is called, either we’ve already constructed self.rowHeights
or we haven’t. If we haven’t, we construct it, by calling attributedStringForIndexPath:
for every index path, laying out its cell rectangle, and storing the height of that rectangle. Now we have all the heights, so from now on we simply return the one we are asked for:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (!self.rowHeights) { NSMutableArray* heights = [NSMutableArray array]; for (int i = 0; i < self.parsedData.items.count; i++) { NSAttributedString* s = [self attributedStringForIndexPath: [NSIndexPath indexPathForRow:i inSection:0]]; CGFloat maxheight = // maximum permissible cell height CGRect r = [s boundingRectWithSize:CGSizeMake(320,maxheight) options: NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:nil]; [heights addObject:@(r.size.height)]; } self.rowHeights = heights; } CGFloat result = [self.rowHeights[indexPath.row] floatValue]; return result; }
In tableView:cellForRowAtIndexPath:
, I call attributedStringForIndexPath:
again. (I suppose I could have cached the attributed strings in an array as well, but this is not a time-consuming routine.) The wonderful thing is that, thanks to dequeueReusableCellWithIdentifier:forIndexPath:
, the cell already has the correct height, and constraints have taken care of positioning its subviews correctly. So I just plop the attributed string into it, configure the rest of the cell, and hand back the cell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { FPItem* item = self.parsedData.items[indexPath.row]; Cell* cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; cell.drawer.attributedText = [self attributedStringForIndexPath: indexPath]; cell.speaker.hidden = !(item.enclosures && [item.enclosures count]); return cell; }
Now I’ll adapt my approach to use estimated heights. My table view has an estimatedRowHeight
of 80; on average, this should set the scrolling content height to be roughly correct from the outset. All I really need to change now is my implementation of tableView:heightForRowAtIndexPath:
, to take account of the fact that I can no longer predict when this method will be called for any given row. If we don’t have any self.rowHeights
array at all, I create it, and I fill it with [NSNull null]
as a placeholder. If the self.rowHeights
entry for the requested row is [NSNull null]
, I calculate the actual row height and set it as that entry. Finally, I return the self.rowHeights
entry for the requested row:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (!self.rowHeights) { NSMutableArray* heights = [NSMutableArray array]; for (int i = 0; i < self.parsedData.items.count; i++) heights[i] = [NSNull null]; self.rowHeights = heights; } int ix = indexPath.row; if (self.rowHeights[indexPath.row] == [NSNull null]) { NSAttributedString* s = [self attributedStringForIndexPath: [NSIndexPath indexPathForRow:ix inSection:0]]; CGFloat maxheight = // maximum permissible cell height CGRect r = [s boundingRectWithSize:CGSizeMake(320,maxheight) options: NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine context:nil]; self.rowHeights[ix] = @(r.size.height); } CGFloat result = [self.rowHeights[ix] floatValue]; return result; }
This method is called four or five times at launch, for the cells that will initially appear onscreen, and after that only for individual cells as they are scrolled into view.
A table view cell has a normal state, a highlighted state (according to its highlighted
property), and a selected state (according to its selected
property). It is possible to change these states directly, possibly with animation, by calling setHighlighted:animated:
or setSelected:animated:
on the cell. But you don’t want to act behind the table’s back, so you are more likely to manage selection through the table view, letting the table view manage and track the state of its cells.
These two states are closely related. In particular, when a cell is selected, it propagates the highlighted state down through its subviews by setting each subview’s highlighted
property if it has one. That is why a UILabel’s highlightedTextColor
applies when the cell is selected. Similarly, a UIImageView (such as the cell’s imageView
) can have a highlightedImage
that is shown when the cell is selected, and a UIControl (such as a UIButton) takes on its highlighted
state when the cell is selected.
One of the chief purposes of your table view is likely to be to let the user select a cell. This will be possible, provided you have not set the value of the table view’s allowsSelection
property to NO. The user taps a normal cell, and the cell switches to its selected state. By default, this will mean that the cell is redrawn with a gray background view, but you can change this at the individual cell level, as I’ve already explained: you can set a cell’s selectedBackgroundView
(or multipleSelectionBackgroundView
), or change its selectionStyle
. If the user taps an already selected cell, by default it stays selected.
Table views can permit the user to select multiple cells simultaneously. Set the table view’s allowsMultipleSelection
property to YES. If the user taps an already selected cell, by default it is deselected.
Your code can learn and manage the selection through these UITableView instance methods:
indexPathForSelectedRow
indexPathsForSelectedRows
indexPathForSelectedRow
when the table view allows multiple selection gives a result that will have you scratching your head in confusion. (As usual, I speak from experience.)
selectRowAtIndexPath:animated:scrollPosition:
The animation involves fading in the selection, but the user may not see this unless the selected row is already visible. The last parameter dictates whether and how the table view should scroll to reveal the newly selected row:
UITableViewScrollPositionTop
UITableViewScrollPositionMiddle
UITableViewScrollPositionBottom
UITableViewScrollPositionNone
For the first three options, the table view scrolls (with animation, if the second parameter is YES) so that the selected row is at the specified position among the visible cells. For UITableViewScrollPositionNone
, the table view does not scroll; if the selected row is not already visible, it does not become visible.
deselectRowAtIndexPath:animated:
To deselect all currently selected rows, call selectRowAtIndexPath:animated:scrollPosition:
with a nil index path. Reloading a cell’s data also deselects that cell, and calling reloadData
deselects all selected rows.
Response to user selection is through the table view’s delegate:
tableView:shouldHighlightRowAtIndexPath:
tableView:didHighlightRowAtIndexPath:
tableView:didUnhighlightRowAtIndexPath:
tableView:willSelectRowAtIndexPath:
tableView:didSelectRowAtIndexPath:
tableView:willDeselectRowAtIndexPath:
tableView:didDeselectRowAtIndexPath:
Despite their names, the two “will” methods are actually “should” methods and expect a return value:
The “highlight” methods are more sensibly named, and they arrive first, so you can return NO from tableView:shouldHighlightRowAtIndexPath:
to prevent a cell from being selected.
Let’s focus in more detail on the relationship between a cell’s highlighted state and its selected state. They are, in fact, two different states. When the user touches a cell, the cell passes through a complete highlight cycle. Then, if the touch turns out to be the beginning of a scroll motion, the cell is unhighlighted immediately, and the cell is not selected. Otherwise, the cell is unhighlighted and selected.
But the user doesn’t know the difference between these two states: whether the cell is highlighted or selected, the cell’s subviews are highlighted, and the selectedBackgroundView
appears. Thus, if the user touches and scrolls, what the user sees is the flash of the selectedBackgroundView
and the highlighted subviews, until the table begins to scroll and the cell returns to normal. If the user touches and lifts the finger, the selectedBackgroundView
and highlighted subviews appear and remain; there is actually a moment in the sequence where the cell has been highlighted and then unhighlighted and not yet selected, but the user doesn’t see any momentary unhighlighting of the cell, because no redraw moment occurs (see Chapter 4).
Here’s a summary of the sequence:
shouldHighlight
permits, the cell highlights, which propagates to its subviews. Then didHighlight
arrives.
selectedBackgroundView
), regardless of what happens next.
The user either starts scrolling or lifts the finger. The cell unhighlights, which also propagates to its subviews, and didUnhighlight
arrives.
willSelect
permits, the cell is selected, and didSelect
arrives. The cell is not highlighted, but highlighting is propagated to its subviews.
selectedBackgroundView
).
When tableView:willSelectRowAtIndexPath:
is called because the user taps a cell, and if this table view permits only single cell selection, tableView:willDeselectRowAtIndexPath:
will be called subsequently for any previously selected cells.
Here’s an example of implementing tableView:willSelectRowAtIndexPath:
. The default behavior for allowsSelection
(not multiple selection) is that the user can select by tapping, and the cell remains selected; if the user taps a selected row, the selection does not change. We can alter this so that tapping a selected row deselects it:
- (NSIndexPath*) tableView:(UITableView*)tv willSelectRowAtIndexPath:(NSIndexPath*)ip { if ([tv cellForRowAtIndexPath:ip].selected) { [tv deselectRowAtIndexPath:ip animated:NO]; return nil; } return ip; }
An extremely common response to user selection is navigation. A master–detail architecture is typical: the table view lists things the user can see in more detail, and a tap displays the detailed view of the selected thing. On the iPhone, very often the table view will be in a navigation interface, and you will respond to user selection by creating the detail view and pushing it onto the navigation controller’s stack.
For example, here’s the code from my Albumen app that navigates from the list of albums to the list of songs in the album that the user has tapped:
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { TracksViewController *t = [[TracksViewController alloc] initWithMediaItemCollection:(self.albums)[indexPath.row]]; [self.navigationController pushViewController:t animated:YES]; }
This interface is so common that Xcode’s Master–Detail Application project template implements it for you.
In a storyboard, when you draw a segue from a UITableViewCell, you are given a choice of two segue triggers: Selection Segue and Accessory Action. If you create a Selection Segue, the segue will be triggered when the user selects a cell. Thus you can readily push or present another view controller in response to cell selection.
If you’re using a UITableViewController, then by default, whenever the table view appears, the selection is cleared automatically in viewWillAppear:
, and the scroll indicators are flashed in viewDidAppear:
. You can prevent this automatic clearing of the selection by setting the table view controller’s clearsSelectionOnViewWillAppear
to NO. I sometimes do that, preferring to implement deselection in viewDidAppear:
; the effect is that when the user returns to the table, the row is still momentarily selected before it deselects itself:
- (void) viewDidAppear:(BOOL)animated { // deselect selected row [tableView selectRowAtIndexPath:nil animated:NO scrollPosition:UITableViewScrollPositionNone]; [super viewDidAppear:animated]; }
By convention, if selecting a table view cell causes navigation, the cell should be given an accessoryType
of UITableViewCellAccessoryDisclosureIndicator
. This is a plain gray right-pointing chevron at the right end of the cell. The chevron itself doesn’t respond to user interaction; it’s not a button, but is just a visual cue that the user can tap the cell to learn more.
Two additional accessoryType
settings are buttons:
UITableViewCellAccessoryDetailButton
UITableViewCellAccessoryDetailDisclosureButton
UITableViewCellAccessoryDetailButton
, along with a disclosure indicator chevron to its right.
To respond to the tapping of an accessory button, implement the table view delegate’s tableView:accessoryButtonTappedForRowWithIndexPath:
. Or, in a storyboard, you can Control-drag a connection from a cell and choose the Accessory Action segue.
A common convention is that selecting the cell as a whole does one thing and tapping the detail button does something else. For example, in Apple’s Phone app, tapping a contact’s listing in the Recents table places a call to that contact, but tapping the detail button navigates to that contact’s detail view.
Another use of cell selection is to implement a choice among cells, where a section of a table effectively functions as an iOS alternative to OS X radio buttons. The table view usually has the grouped format. An accessoryType
of UITableViewCellAccessoryCheckmark
is typically used to indicate the current choice. Implementing radio button behavior is up to you.
As an example, I’ll implement the interface shown in Figure 8-2. The table view has the grouped style, with two sections. The first section, with a “Size” header, has three mutually exclusive choices: “Easy,” “Normal,” and “Hard.” The second section, with a “Style” header, has two choices: “Animals” or “Snacks.”
This is a static table; its contents are known beforehand and won’t change. In a case like this, if we’re using a UITableViewController subclass instantiated from a storyboard, the nib editor lets us design the entire table, including the headers and the cells and their content, directly in the storyboard. Select the table and set its Content pop-up menu in the Attributes inspector to Static Cells to make the table editable in this way (Figure 8-6).
Even though each cell is designed initially in the storyboard, I can still implement tableView:cellForRowAtIndexPath:
to call super
and then add further functionality. That’s how I’ll add the checkmarks. The user defaults are storing the current choice in each of the two categories; there’s a @"Size"
preference and a @"Style"
preference, each consisting of a string denoting the title of the chosen cell:
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell* cell = [super tableView:tv cellForRowAtIndexPath:indexPath]; NSUserDefaults* ud = [NSUserDefaults standardUserDefaults]; cell.accessoryType = UITableViewCellAccessoryNone; if ([[ud valueForKey:@"Style"] isEqualToString:cell.textLabel.text] || [[ud valueForKey:@"Size"] isEqualToString:cell.textLabel.text]) cell.accessoryType = UITableViewCellAccessoryCheckmark; return cell; }
When the user taps a cell, the cell is selected. I want the user to see that selection momentarily, as feedback, but then I want to deselect, adjusting the checkmarks so that that cell is the only one checked in its section. In tableView:didSelectRowAtIndexPath:
, I set the user defaults, and then I reload the table view’s data. This removes the selection and causes tableView:cellForRowAtIndexPath:
to be called to adjust the checkmarks:
- (void)tableView:(UITableView *)tv didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSUserDefaults* ud = [NSUserDefaults standardUserDefaults]; NSString* setting = [tv cellForRowAtIndexPath:indexPath].textLabel.text; NSString* header = [self tableView:tv titleForHeaderInSection:indexPath.section]; [ud setValue:setting forKey:header]; [tv reloadData]; // deselect all cells, reassign checkmark as needed }
A UITableView is a UIScrollView, so everything you already know about scroll views is applicable (Chapter 7). In addition, a table view supplies two convenience scrolling methods:
scrollToRowAtIndexPath:atScrollPosition:animated:
scrollToNearestSelectedRowAtScrollPosition:animated:
The scrollPosition
parameter is as for selectRowAtIndexPath:...
, discussed earlier in this chapter.
The following UITableView methods mediate between the table’s bounds coordinates on the one hand and table structure on the other:
indexPathForRowAtPoint:
indexPathsForRowsInRect:
rectForSection:
rectForRowAtIndexPath:
rectForFooterInSection:
rectForHeaderInSection:
The table’s own header and footer are direct subviews of the table view, so their positions within the table’s bounds are given by their frames.
If a UITableView participates in state saving and restoration (Chapter 6), the restoration mechanism would like to restore the selection and the scroll position. In iOS 7, this behavior is automatic; the restoration mechanism knows both what cells should be visible and what cells should be selected, in terms of their index paths. If that’s satisfactory, you’ve no further work to do.
In some apps, however, there is a possibility that when the app is relaunched, the underlying data may have been rearranged somehow. Perhaps what’s meaningful in dictating what the user should see in such a case is not the previous index paths but the previous data. The state saving and restoration mechanism doesn’t know anything about the relationship between the cells and the underlying data. If you’d like to tell it, adopt the UIDataSourceModelAssociation protocol and implement two methods:
modelIdentifierForElementAtIndexPath:inView:
indexPathForElementWithModelIdentifier:inView:
Devising a system of unique identification and incorporating it into your data model is up to you. In the TidBITS News app, for example, where I was using this mechanism in iOS 6, it happens that my bits of data come from a parsed RSS feed and have a guid
property that is a global unique identifier. So implementing the first method is easy:
- (NSString *) modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view { FPItem* item = self.parsedData.items[idx.row]; return item.guid; }
Implementing the second method is a little more work; I walk the data model looking for the object whose guid
matches the identifier in question, and construct its index path:
- (NSIndexPath*) indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { __block NSIndexPath* path = nil; [self.parsedData.items enumerateObjectsUsingBlock:^(FPItem* item, NSUInteger idx, BOOL *stop) { if ([item.guid isEqualToString:identifier]) { path = [NSIndexPath indexPathForRow:idx inSection:0]; *stop = YES; } }]; return path; }
It is crucial, when the app is relaunched, that the table should have data before that method is called, so I also call reloadData
in my implementation of decodeRestorableStateWithCoder:
.
In iOS 7, however, I no longer do any of that, since the automatic index path–based restoration of the table view’s scroll position is sufficient.
A table view is a common way to present the results of a search performed through a search field (a UISearchBar; see Chapter 12). This is such a standard interface, in fact, that a class is provided, UISearchDisplayController, to mediate between the search field where the user enters a search term and the table view listing the results of the search.
When the user initially taps in the search field, the UISearchDisplayController automatically constructs a new interface along with a nice animation. This indicates to the user that the search field is ready to receive input; when the user proceeds to enter characters into the search field, the UISearchDisplayController will display its own search results table view in this interface. The UISearchBar has a Cancel button that the user can tap to dismiss the interface created by the UISearchDisplayController.
The UISearchDisplayController needs the following things:
searchBar
.
searchContentsController
.
searchResultsTableView
. It can already exist, or the UISearchDisplayController will create it.
searchResultsDataSource
and searchResultsDelegate
. They will control the data and structure of the search results table. They are commonly the same object, as for any table view; moreover, they are commonly the same as the view controller (the searchContentsController
).
An optional object adopting the UISearchDisplayDelegate protocol. It will be notified of events relating to the display of results. It, too, is commonly the view controller (the searchContentsController
).
Moreover, the UISearchBar itself can also have a delegate, and this, too, is commonly the view controller (the searchContentsController
).
A UISearchDisplayController’s searchContentsController
needn’t be a UITableViewController, and the data that the user is searching needn’t be the content of an existing table view. But they frequently are! That’s because the mental connection between a table and a search is a natural one; when the search results are presented as a table view, the user feels that the search field is effectively filtering the contents of the original table view. A single object may thus be playing all of the following roles:
A common point of confusion among beginners, when using this architecture, is to suppose that the search bar is genuinely filtering the original table. It isn’t! The search bar and the UISearchDisplayController know nothing of your table. What’s being searched is just some data — whatever data you care to search. The fact that this may be the model data for your table is purely secondary.
Moreover, there are two distinct tables: yours (the original table view) and the UISearchDisplayController’s (the search results table view). You own the former, just as you would if no search were involved; you probably have a view controller that manages it, very likely a UITableViewController whose tableView
is this table. But the search results table is a completely different table; you do not have a view controller managing it (the UISearchDisplayController does), and in particular it is not your UITableViewController’s tableView
. However, if you wish, you can make it look as if these are the same table, by configuring the two tables and their cells the same way — typically, with the same code.
To illustrate, we will implement a table view that is searchable through a UISearchBar and that displays the results of that search in a second table view managed by a UISearchDisplayController.
The first question is how to make the search field appear along with the table view.
One approach is to make the search field be the table view’s header view. Indeed, this is such a common arrangement that, in the nib editor, if you drag a Search Bar and Search Display Controller object onto a UITableView, the search field automatically becomes the table’s header view and a UISearchDisplayController is created for you with all properties hooked up appropriately through outlets, much as I just described.
It is more educational, however, to create the UISearchDisplayController and the UISearchBar in code. So let’s do that. I’ll adapt my earlier example displaying the names of states in a section-based table, so that it becomes searchable.
We already have a table managed by a UITableViewController. In viewDidLoad
, we create the search bar and slot it in as the table’s header view; we then load the data and scroll the header view out of sight. We also create the UISearchDisplayController and tie it to the search bar — and to ourselves (the UITableViewController) as the UISearchDisplayController’s controller, delegate, search table data source, and search table delegate, as well as making ourselves the UISearchBar delegate. We also retain the UISearchDisplayController by assigning it to a property, so that it doesn’t vanish in a puff of smoke before we can use it:
UISearchBar* b = [UISearchBar new]; [b sizeToFit]; b.autocapitalizationType = UITextAutocapitalizationTypeNone; b.delegate = self; [self.tableView setTableHeaderView:b]; [self.tableView reloadData]; [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO]; UISearchDisplayController* c = [[UISearchDisplayController alloc] initWithSearchBar:b contentsController:self]; self.sdc = c; // retain the UISearchDisplayController c.delegate = self; c.searchResultsDataSource = self; c.searchResultsDelegate = self;
After the UISearchDisplayController is configured to use our view controller (self
) as its contentsController
, it is also pointed to by our view controller’s built-in searchDisplayController
read-only property. But that property does not retain it, as the documentation misleadingly implies; you still need to assign it to a strong property of your own.
Populating the search results table in response to what the user does in the UISearchBar is up to us. The amazing thing is that this is almost working already, because we have been fiendishly clever at the outset: the UITableViewController is both data source and delegate for the original table view, as well as data source and delegate for the search results table view, so by default the search results table will portray the same data as the original table!
It isn’t quite working, however. We need to make a few tweaks.
As the UISearchDisplayController’s table view comes into existence, we get a delegate message. This is the place to perform any initial configurations on the UISearchDisplayController’s table view. For example, if my viewDidLoad
is setting my table view’s separator style to UITableViewCellSeparatorStyleNone
, and if I want the two tables to look identical, this would be the place to set the UISearchDisplayController’s table view’s separator style to UITableViewCellSeparatorStyleNone
as well. In this example, I’ll take advantage of this moment to suppress display of headers in the search results table:
-(void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView { // configure search results table to look like our table tableView.sectionHeaderHeight = 0; }
Next, let’s talk about where the search results table is going to get its cells. At present, the original table gets its cells like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
But when the search results table calls into this same delegate method, tableView
will be the search results table. That’s wrong. We don’t want to use cells from the search results table; we want to use cells from the original table, so that they’ll be structured like those of the original table. So we change tableView
to self.tableView
, referring explicitly to our original table as the source of cells:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
Our search interface has now sprung to life! We can type in the search field, and a search results table appears. It looks just like our original table, except that it has no sections. The only problem is that the search bar itself is not doing anything. Instead of searching, we’re displaying all the same data as the original table.
Clearly, we need to check whether the table view that’s talking to us is the search results table view (this will be the UISearchDisplayController’s searchResultsTableView
). If it is, we want to limit our returned data with respect to the search bar’s text. The strategy for doing this should be fairly obvious if we are maintaining our source data in a sensible model.
Recall that our original table is displaying the names of the 50 United States, which it is getting from a mutable array of mutable arrays of strings called sectionData
. Let’s restructure our responses to the Three Big Questions so that they refer to this array as model
, like this:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.sectionNames count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSArray* model = self.sectionData; return [model[section] count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; NSArray* model = self.sectionData; NSString* s = model[indexPath.section][indexPath.row]; cell.textLabel.text = s; return cell; }
To implement searching, each time we speak of the NSArray called model
, we will decide whether it should be self.sectionData
, as now, or whether it should be a different array that is filtered with respect to the current search — let’s call it self.filteredSectionData
. There are two occurrences of this line:
NSArray* model = self.sectionData;
They are now to be replaced by this:
NSArray* model = (tableView == self.sdc.searchResultsTableView) ? self.filteredSectionData : self.sectionData;
The remaining question is when and how this self.filteredSectionData
array should be calculated. An excellent approach, given our small and readily available data set, is to generate a new set of search results every time the user types in the search field, effectively implementing a “live” search (Figure 8-7). We are informed of the user typing through a UISearchBar delegate method, searchBar:textDidChange:
, so we implement this to generate self.filteredSectionData
freshly. There is no need to reload the search results table’s data, as by default the UISearchDisplayController will do that automatically:
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { // this is the search criteria NSPredicate* p = [NSPredicate predicateWithBlock: ^BOOL(id obj, NSDictionary *d) { NSString* s = obj; return ([s rangeOfString:searchText options:NSCaseInsensitiveSearch].location != NSNotFound); }]; // generate filtered section data NSMutableArray* filteredData = [NSMutableArray new]; // sectionData is an array of arrays; for every array... for (NSMutableArray* arr in self.sectionData) { // generate an array of strings passing the search criteria [filteredData addObject: [arr filteredArrayUsingPredicate:p]]; } self.filteredSectionData = filteredData; }
Our search interface is now working! In the rest of this section, I’ll discuss some additional optional tweaks.
A UISearchBar can display scope buttons, letting the user alter the meaning of the search. If you add these, then of course you must take them into account when filtering the model data. For example, let’s have two scope buttons, “Starts With” and “Contains”:
UISearchBar* b = [UISearchBar new]; [b sizeToFit]; b.scopeButtonTitles = @[@"Starts With", @"Contains"];
Our filtering routine must now take the state of the scope buttons into account. Moreover, the search results table view will reload when the user changes the scope; we can detect this in another UISearchBar delegate method, searchBar:selectedScopeButtonIndexDidChange:
. If we’re doing a live search, we must respond by filtering the data then as well. To prevent repetition, we’ll abstract the filtering routine into a method of its own:
- (void) filterData: (UISearchBar*) sb { NSPredicate* p = [NSPredicate predicateWithBlock: ^BOOL(id obj, NSDictionary *d) { NSString* s = obj; NSStringCompareOptions options = NSCaseInsensitiveSearch; if (sb.selectedScopeButtonIndex == 0) options |= NSAnchoredSearch; return ([s rangeOfString:sb.text options:options].location != NSNotFound); }]; NSMutableArray* filteredData = [NSMutableArray new]; for (NSMutableArray* arr in self.sectionData) { [filteredData addObject: [arr filteredArrayUsingPredicate:p]]; } self.filteredSectionData = filteredData; } - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { [self filterData: searchBar]; } - (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope { [self filterData: searchBar]; }
In our original table, the search bar is initially scrolled out of sight. Let’s make it easier for the user to discover its existence and summon it. In an indexed list — one with sections and an index running down the right side — a “magnifying glass” search symbol can be made to appear in the index by including UITableViewIndexSearch
(usually as the first item) in the string array returned from sectionIndexTitlesForTableView:
. The section names are already in an array called sectionNames
:
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { if (tableView != self.tableView) return nil; return [@[UITableViewIndexSearch] arrayByAddingObjectsFromArray:self.sectionNames]; }
The magnifying glass appears in the section index, but now there’s a bug: the correspondence between index entries and sections is off by one. To fix that, we need to implement tableView:sectionForSectionIndexTitle:atIndex:
. If the user taps on or above the magnifying glass in the index, we scroll to reveal the search field (and we’ll also have to return a bogus section number, but there is no penalty for that):
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index { if (index == 0) [tableView scrollRectToVisible:tableView.tableHeaderView.frame animated:NO]; return index-1; }
Whenever the search results table becomes empty (because the search bar is nonempty and self.filteredSectionData
is nil), the words “No Results” appear in a label superimposed on it. I find this label incredibly obnoxious; yet, after all these years, Apple still hasn’t granted programmers an official way to remove or customize it. Here’s an unofficial way:
-(BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString { dispatch_async(dispatch_get_main_queue(), ^{ for (UIView* v in controller.searchResultsTableView.subviews) { if ([v isKindOfClass: [UILabel class]] && [[(UILabel*)v text] isEqualToString:@"No Results"]) { [(UILabel*)v setText: @""]; break; } } }); return YES; }
New in iOS 7, if our view controller (the UISearchDisplayController’s searchContentsController
) is the child of a UINavigationController, we can make the search bar appear in the navigation bar. To do so, set the UISearchDisplayController’s displaysSearchBarInNavigationBar
property to YES. If the UISearchDisplayController also has a navigationItem
, it is used instead of our view controller’s navigationItem
to populate the navigation bar.
This doesn’t mean that you can toggle displaysSearchBarInNavigationBar
to make a search bar appear and disappear in your navigation bar. On the contrary, the UISearchDisplayController must be created early, and the search bar then will always be present in the navigation bar for this view controller. However, you’re in a navigation interface, so you can easily work around this limitation by navigating to and from the searchable view controller.
Also, a search bar in a navigation bar can’t have scope buttons, and its Cancel button doesn’t come and go automatically — either it is constantly present (because you set its showsCancelButton
to YES) or it never appears.
This example code is from our view controller’s viewDidLoad
:
UISearchBar* b = [UISearchBar new]; b.delegate = self; b.showsCancelButton = YES; b.autocapitalizationType = UITextAutocapitalizationTypeNone; UISearchDisplayController* c = [[UISearchDisplayController alloc] initWithSearchBar:b contentsController:self]; self.sdc = c; // retain the UISearchDisplayController c.delegate = self; c.searchResultsDataSource = self; c.searchResultsDelegate = self; c.displaysSearchBarInNavigationBar = YES;
The result is that our navigation bar’s title view is the search bar. The back bar button item, if any, is shown, so the user can pop this view controller in the usual way. Any other buttons you want to have appear in the navigation bar must added to the UISearchDisplayController’s navigationItem
(not the view controller’s navigationItem
, which is ignored).
A UISearchBar has many properties through which its appearance can be configured; I’ll discuss them in Chapter 12. Both the UISearchBar and UISearchDisplayController send their delegate numerous messages that you can take advantage of to customize behavior; consult the documentation. A UISearchBar in a UIToolbar on the iPad can display its results in a popover; I’ll talk about that in Chapter 9.
A table view cell has a normal state and an editing state, according to its editing
property.
The editing state is typically indicated visually by one or more of the following:
tableView:shouldIndentWhileEditingRowAtIndexPath:
. (There is also a cell property shouldIndentWhileEditing
, but I find it unreliable.)
editingAccessoryType
or editingAccessoryView
. If you assign neither, so that they are nil, the cell’s existing accessory view will vanish when in editing mode.
As with selection, you could set a cell’s editing
property directly (or use setEditing:animated:
to get animation), but you are more likely to let the table view manage editability. Table view editability is controlled through the table view’s editing
property, usually by sending the table the setEditing:animated:
message. The table is then responsible for putting its cells into edit mode.
A cell in edit mode can also be selected by the user if the table view’s allowsSelectionDuringEditing
or allowsMultipleSelectionDuringEditing
is YES. But this would be unusual.
Putting the table into edit mode is usually left up to the user. A typical interface would be an Edit button that the user can tap. In a navigation interface, we might have our view controller supply the button as the navigation item’s right button:
UIBarButtonItem* bbi = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:@selector(doEdit:)]; self.navigationItem.rightBarButtonItem = bbi;
Our action handler will be responsible for putting the table into edit mode, so in its simplest form it might look like this:
- (void) doEdit: (id) sender { [self.tableView setEditing:YES animated:YES]; }
But that does not solve the problem of getting out of editing mode. The standard solution is to have the Edit button replace itself by a Done button:
- (void) doEdit: (id) sender { int which; if (![self.tableView isEditing]) { [self.tableView setEditing:YES animated:YES]; which = UIBarButtonSystemItemDone; } else { [self.tableView setEditing:NO animated:YES]; which = UIBarButtonSystemItemEdit; } UIBarButtonItem* bbi = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:which target:self action:@selector(doEdit:)]; self.navigationItem.rightBarButtonItem = bbi; }
However, it turns out that all of that is completely unnecessary! If we want standard behavior, it’s already implemented for us. A UIViewController supplies an editButtonItem
that calls the UIViewController’s setEditing:animated:
when tapped, tracks whether we’re in edit mode with the UIViewController’s editing
property, and changes its own title accordingly (Edit or Done). Moreover, a UITableViewController’s implementation of setEditing:animated:
is to call setEditing:animated:
on its table view. Thus, if we’re using a UITableViewController, we get all of that behavior for free, just by inserting the editButtonItem
into our interface:
self.navigationItem.rightBarButtonItem = self.editButtonItem;
When the table view enters edit mode, it consults its data source and delegate about the editability of individual rows:
tableView:canEditRowAtIndexPath:
to the data source
tableView:editingStyleForRowAtIndexPath:
to the delegate
Each standard editing style corresponds to a control that will appear in the cell. The choices are:
UITableViewCellEditingStyleDelete
UITableViewCellEditingStyleInsert
UITableViewCellEditingStyleNone
If the user taps an insert button (the Plus button) or a delete button (the Delete button that appears after the user taps the Minus button), the data source is sent the tableView:commitEditingStyle:forRowAtIndexPath:
message and is responsible for obeying it. In your response, you will probably want to alter the structure of the table, and UITableView methods for doing this are provided:
insertRowsAtIndexPaths:withRowAnimation:
deleteRowsAtIndexPaths:withRowAnimation:
insertSections:withRowAnimation:
deleteSections:withRowAnimation:
moveSection:toSection:
moveRowAtIndexPath:toIndexPath:
The row animations here are effectively the same ones discussed earlier in connection with refreshing table data; “left” for an insertion means to slide in from the left, and for a deletion it means to slide out to the left, and so on. The two “move” methods provide animation with no provision for customizing it.
If you’re issuing more than one of these commands, you can combine them by surrounding them with beginUpdates
and endUpdates
, forming an updates block. An updates block combines not just the animations but the requested changes themselves. This relieves you from having to worry about how a command is affected by earlier commands in the same updates block; indeed, order of commands within an updates block doesn’t really matter.
For example, if you delete row 1 of a certain section and then (in a separate command) delete row 2 of the same section, you delete two successive rows, just as you would expect; the notion “2” does not change its meaning because you deleted an earlier row first, because you didn’t delete an earlier row first — the updates block combines the commands for you, interpreting both index paths with respect to the state of the table before any changes are made. If you perform insertions and deletions together in one animation, the deletions are performed first, regardless of the order of your commands, and the insertion row and section numbers refer to the state of the table after the deletions.
An updates block can also include reloadRows...
and reloadSections...
commands (but not reloadData
).
I need hardly emphasize once again (but I will anyway) that view is not model. It is one thing to rearrange the appearance of the table, another to alter the underlying data. It is up to you to make certain you do both together. Do not, even for a moment, permit the data and the view to get out of synch with each other. If you delete a row, remove from the model the datum that it represents. The runtime will try to help you with error messages if you forget to do this, but in the end the responsibility is yours. I’ll give examples as we proceed.
Deletion of table items is the default, so there’s not much for us to do in order to implement it. If our view controller is a UITableViewController and we’ve displayed the Edit button as its navigation item’s right button, everything happens automatically: the user taps the Edit button, the view controller’s setEditing:animated:
is called, the table view’s setEditing:animated:
is called, and the cells all show the Minus button at the left end. The user can then tap a Minus button; a Delete button is shown at the cell’s right end. You can customize the Delete button’s title with the table delegate method tableView:titleForDeleteConfirmationButtonForRowAtIndexPath:
.
What is not automatic is the actual response to the Delete button. For that, we need to implement tableView:commitEditingStyle:forRowAtIndexPath:
. Typically, you’ll remove the corresponding entry from the underlying model data, and you’ll call deleteRowsAtIndexPaths:withRowAnimation:
or deleteSections:withRowAnimation:
to update the appearance of the table. As I said a moment ago, you must delete the row or section in such a way as to keep the table display coordinated with the model’s structure. Otherwise, the app may crash (with an extremely helpful error message).
To illustrate, let’s suppose once again that the underlying model is a pair of parallel arrays, a mutable array of strings (sectionNames
) and a mutable array of arrays (sectionData
). Our approach will be in two stages:
deleteSections:withRowAnimation:
(and reload the section index if there is one); otherwise, we’ll call deleteRowsAtIndexPaths:withRowAnimation:
:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)ip { [self.sectionData[ip.section] removeObjectAtIndex:ip.row]; if ([self.sectionData[ip.section] count] == 0) { [self.sectionData removeObjectAtIndex: ip.section]; [self.sectionNames removeObjectAtIndex: ip.section]; [tableView deleteSections:[NSIndexSet indexSetWithIndex: ip.section] withRowAnimation:UITableViewRowAnimationAutomatic]; [tableView reloadSectionIndexTitles]; } else { [tableView deleteRowsAtIndexPaths:@[ip] withRowAnimation:UITableViewRowAnimationAutomatic]; } }
The user can also delete a row by sliding it to the left to show its Delete button without having explicitly entered edit mode; no other row is editable, and no other editing controls are shown. (In iOS 7, the table cell is itself inside a little horizontal scroll view; the user is actually scrolling the cell to the left, revealing the Delete button behind it.) This feature is implemented “for free” by virtue of our having supplied an implementation of tableView:commitEditingStyle:forRowAtIndexPath:
. If you’re like me, your first response will be: “Thanks for the free functionality, Apple, and now how do I turn this off?” Because the Edit button is already using the UIViewController’s editing
property to track edit mode, we can take advantage of this and refuse to let any cells be edited unless the view controller is in edit mode:
- (UITableViewCellEditingStyle)tableView:(UITableView *)aTableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { return self.editing ? UITableViewCellEditingStyleDelete : UITableViewCellEditingStyleNone; }
A table item might have content that the user can edit directly, such as a UITextField (Chapter 10). Because the user is working in the view, you need a way to reflect the user’s changes into the model. This will probably involve putting yourself in contact with the interface objects where the user does the editing.
To illustrate, I’ll implement a table view cell with a text field that is editable when the cell is in editing mode. Imagine an app that maintains a list of names and phone numbers. A name and phone number are displayed as a grouped style table, and they become editable when the user taps the Edit button (Figure 8-8).
We don’t need a button at the left end of the cell when it’s being edited:
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewCellEditingStyleNone; }
A UITextField is editable if its enabled
is YES. To tie this to the cell’s editing
state, it is probably simplest to implement a custom UITableViewCell class. I’ll call it MyCell, and I’ll design it in the nib, giving it a single UITextField that’s pointed to through a property called textField
. In the code for MyCell, we override didTransitionToState:
, as follows:
- (void) didTransitionToState:(UITableViewCellStateMask)state { [super didTransitionToState:state]; if (state == UITableViewCellStateEditingMask) { self.textField.enabled = YES; } if (state == UITableViewCellStateDefaultMask) { self.textField.enabled = NO; } }
In the table’s data source, we make ourselves the text field’s delegate when we create and configure the cell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; if (indexPath.section == 0) cell.textField.text = self.name; if (indexPath.section == 1) { cell.textField.text = self.numbers[indexPath.row]; cell.textField.keyboardType = UIKeyboardTypeNumbersAndPunctuation; } cell.textField.delegate = self; return cell; }
We are the UITextField’s delegate, so we are responsible for implementing the Return button in the keyboard to dismiss the keyboard (I’ll talk more about this in Chapter 10):
- (BOOL)textFieldShouldReturn:(UITextField *)tf { [tf endEditing:YES]; return NO; }
When a text field stops editing, we are its delegate, so we can hear about it in textFieldDidEndEditing:
. We work out which cell it belongs to, and update the model accordingly:
- (void)textFieldDidEndEditing:(UITextField *)tf { // some cell's text field has finished editing; which cell? UIView* v = tf; do { v = v.superview; } while (![v isKindOfClass: [UITableViewCell class]]); MyCell* cell = (MyCell*)v; // update data model to match NSIndexPath* ip = [self.tableView indexPathForCell:cell]; if (ip.section == 1) self.numbers[ip.row] = cell.textField.text; else if (ip.section == 0) self.name = cell.textField.text; }
You are unlikely to attach a Plus (insert) button to every row. A more likely interface is that when a table is edited, every row has a Minus button except the last row, which has a Plus button; this shows the user that a new row can be appended at the end of the list.
Let’s implement this for phone numbers in our name-and-phone-number app, allowing the user to give a person any quantity of phone numbers (Figure 8-9):
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 1) { NSInteger ct = [self tableView:tableView numberOfRowsInSection:indexPath.section]; if (ct-1 == indexPath.row) return UITableViewCellEditingStyleInsert; return UITableViewCellEditingStyleDelete; } return UITableViewCellEditingStyleNone; }
The person’s name has no editing control (a person must have exactly one name), so we prevent it from indenting in edit mode:
- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 1) return YES; return NO; }
When the user taps an editing control, we must respond. We immediately force our text fields to cease editing: the user have may tapped the editing control while editing, and we want our model to contain the very latest changes, so this is effectively a way of causing our textFieldDidEndEditing:
to be called. The model for our phone numbers is a mutable array of strings (self.numbers
). We already know what to do when the tapped control is a delete button; things are similar when it’s an insert button, but we’ve a little more work to do. The new row will be empty, and it will be at the end of the table; so we append an empty string to the self.numbers
model array, and then we insert a corresponding row at the end of the view. But now two successive rows have a Plus button; the way to fix that is to reload the first of those rows. Finally, we also show the keyboard for the new, empty phone number, so that the user can start editing it immediately; we do that outside the updates block:
- (void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { [tableView endEditing:YES]; if (editingStyle == UITableViewCellEditingStyleInsert) { [self.numbers addObject: @""]; NSInteger ct = [self.numbers count]; [tableView beginUpdates]; [tableView insertRowsAtIndexPaths: @[[NSIndexPath indexPathForRow: ct-1 inSection:1]] withRowAnimation:UITableViewRowAnimationAutomatic]; [self.tableView reloadRowsAtIndexPaths: @[[NSIndexPath indexPathForRow:ct-2 inSection:1]] withRowAnimation:UITableViewRowAnimationAutomatic]; [tableView endUpdates]; // crucial that this next bit be *outside* the updates block UITableViewCell* cell = [self.tableView cellForRowAtIndexPath: [NSIndexPath indexPathForRow:ct-1 inSection:1]]; [((MyCell*)cell).textField becomeFirstResponder]; } if (editingStyle == UITableViewCellEditingStyleDelete) { [self.numbers removeObjectAtIndex:indexPath.row]; [tableView beginUpdates]; [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; [tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationAutomatic]; [tableView endUpdates]; } }
If the data source implements tableView:moveRowAtIndexPath:toIndexPath:
, the table displays a reordering control at the right end of each row in editing mode (Figure 8-9), and the user can drag it to rearrange table items. The reordering control can be suppressed for individual table items by implementing tableView:canMoveRowAtIndexPath:
. The user is free to move rows that display a reordering control, but the delegate can limit where a row can be moved to by implementing tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath:
.
To illustrate, we’ll add to our name-and-phone-number app the ability to rearrange phone numbers. There must be multiple phone numbers to rearrange:
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 1 && [self.numbers count] > 1) return YES; return NO; }
A phone number must not be moved out of its section, so we implement the delegate method to prevent this. We also take this opportunity to dismiss the keyboard if it is showing.
- (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath*)sourceIndexPath toProposedIndexPath:(NSIndexPath*)proposedDestinationIndexPath { [tableView endEditing:YES]; if (proposedDestinationIndexPath.section == 0) return [NSIndexPath indexPathForRow:0 inSection:1]; return proposedDestinationIndexPath; }
After the user moves an item, tableView:moveRowAtIndexPath:toIndexPath:
is called, and we trivially update the model to match. We also reload the table, to fix the editing controls:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { NSString* s = self.numbers[fromIndexPath.row]; [self.numbers removeObjectAtIndex: fromIndexPath.row]; [self.numbers insertObject:s atIndex: toIndexPath.row]; [tableView reloadData]; }
A table may be rearranged not just in response to the user working in edit mode, but for some other reason entirely. In this way, many interesting and original interfaces are possible.
In this example, we permit the user to double tap on a section header as a way of collapsing or expanding the section — that is, we’ll suppress or permit the display of the rows of the section, with a nice animation as the change takes place. (This idea is shamelessly stolen from a WWDC 2010 video.)
One more time, our data model consists of the two arrays, sectionNames
and sectionData
. I’ve also got an NSMutableSet, hiddenSections
, in which I’ll list the sections that aren’t displaying their rows. That list is all I’ll need, since either a section is showing all its rows or it’s showing none of them:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if ([self.hiddenSections containsObject:@(section)]) return 0; return [self.sectionData[section] count]; }
We need a correspondence between a section header and the number of its section. It’s odd that UITableView doesn’t give us such a correspondence; it provides indexPathForCell:
, but there is no sectionForHeaderFooterView:
. My solution is to subclass UITableViewHeaderFooterView and give my subclass a public property section
, to which I assign the current section number whenever tableView:viewForHeaderInSection:
is called:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { MyHeaderView* h = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"Header"]; // ... h.section = section; return h; }
The section headers are a UITableViewHeaderFooterView subclass with userInteractionEnabled
set to YES and a UITapGestureRecognizer attached, so we can detect a double tap. When the user double taps a section header, we learn from the header what section this is; we also find out from the model how many rows this section has, as we’ll need to know that later, regardless of whether we’re about to show or hide rows. Then we look for the section number in our hiddenSections
set. If it’s there, we’re about to display the rows, so we remove that section number from hiddenSections
; now we work out the index paths of the rows we’re about to insert, and we insert them. If it’s not there, we’re about to hide the rows, so we insert that section number into hiddenSections
; again, we work out the index paths of the rows we’re about to delete, and we delete them:
- (void) tap: (UIGestureRecognizer*) g { UITableViewHeaderFooterView* v = (id)g.view; NSUInteger sec = v.section; NSUInteger ct = [(NSArray*)(self.sectionData)[sec] count]; NSNumber* secnum = @(sec); if ([self.hiddenSections containsObject:secnum]) { [self.hiddenSections removeObject:secnum]; [self.tableView beginUpdates]; NSMutableArray* arr = [NSMutableArray array]; for (int ix = 0; ix < ct; ix ++) { NSIndexPath* ip = [NSIndexPath indexPathForRow:ix inSection:sec]; [arr addObject: ip]; } [self.tableView insertRowsAtIndexPaths:arr withRowAnimation:UITableViewRowAnimationAutomatic]; [self.tableView endUpdates]; [self.tableView scrollToRowAtIndexPath:[arr lastObject] atScrollPosition:UITableViewScrollPositionNone animated:YES]; } else { [self.hiddenSections addObject:secnum]; [self.tableView beginUpdates]; NSMutableArray* arr = [NSMutableArray array]; for (int ix = 0; ix < ct; ix ++) { NSIndexPath* ip = [NSIndexPath indexPathForRow:ix inSection:sec]; [arr addObject: ip]; } [self.tableView deleteRowsAtIndexPaths:arr withRowAnimation:UITableViewRowAnimationAutomatic]; [self.tableView endUpdates]; } }
A menu, in iOS, is a sort of balloon containing tappable words such as Copy, Cut, and Paste. You can permit the user to display a menu from a table view cell by performing a long press on the cell. The long press followed by display of the menu gives the cell a selected appearance, but in iOS 7, the selected appearance goes away when the menu is dismissed.
To allow the user to display a menu from a table view’s cells, you implement three delegate methods:
tableView:shouldShowMenuForRowAtIndexPath:
tableView:canPerformAction:forRowAtIndexPath:withSender:
copy:
, cut:
, and paste:
actions; return NO to prevent the menu item for an action from appearing. The menu itself will then appear unless you return NO to all three actions. The sender is the shared UIMenuController, which I’ll discuss more in Chapter 10 and Chapter 26.
tableView:performAction:forRowAtIndexPath:withSender:
Here’s an example where the user can summon a Copy menu from any cell (Figure 8-10):
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath { return YES; } - (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { return (action == NSSelectorFromString(@"copy:")); } - (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { NSString* s = self.sectionData[indexPath.section][indexPath.row]; if (action == NSSelectorFromString(@"copy:")) { // ... do whatever copying consists of ... } }
To add a custom menu item to the menu (other than copy:
, cut:
, and paste:
) is a little more work. First, you must tell the shared UIMenuController to append the menu item to the global menu; the tableView:shouldShowMenuForRowAtIndexPath:
delegate method is a good place to do this:
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath { // extra menu item UIMenuItem* mi = [[UIMenuItem alloc] initWithTitle:@"Abbrev" action:NSSelectorFromString(@"abbrev:")]; [[UIMenuController sharedMenuController] setMenuItems:@[mi]]; return YES; }
We have now given the menu an additional menu item whose title is Abbrev, and whose action when the menu item is tapped is abbrev:
. (I am imagining here a table of the names of U.S. states, where one can copy a state’s two-letter abbreviation to the clipboard.) If we want this menu item to appear in the menu, and if we want to respond to it when the user taps it, we must add that selector to the two performAction:
delegate methods:
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { return (action == NSSelectorFromString(@"copy:") || action == NSSelectorFromString(@"abbrev:")); } - (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { NSString* s = self.sectionData[indexPath.section][indexPath.row]; if (action == NSSelectorFromString(@"copy:")) { // ... do whatever copying consists of ... NSLog(@"copying %@", s); } if (action == NSSelectorFromString(@"abbrev:")) { // ... do whatever abbreviating consists of ... NSLog(@"abbreviating %@", s); } }
Now comes the tricky part: we must implement our custom selector, abbrev:
, in the cell. We will therefore need our table to use a custom UITableViewCell subclass. Let’s call it MyCell:
#import "MyCell.h" @implementation MyCell -(void)abbrev:(id)sender { // ... } @end
The Abbrev menu item now appears when the user long-presses a cell of our table, and the cell’s abbrev:
method is called when the user taps that menu item. We could respond directly to the tap in the cell, but it seems more consistent that our table view delegate should respond. So we work out what table view this cell belongs to and send its delegate the very message it is already expecting:
-(void)abbrev:(id)sender { // find my table view UIView* v = self; do { v = v.superview; } while (![v isKindOfClass:[UITableView class]]); UITableView* tv = (UITableView*) v; // ask it what index path we are NSIndexPath* ip = [tv indexPathForCell:self]; // talk to its delegate if (tv.delegate && [tv.delegate respondsToSelector: @selector(tableView:performAction:forRowAtIndexPath:withSender:)]) [tv.delegate tableView:tv performAction:_cmd forRowAtIndexPath:ip withSender:sender]; }
A collection view (UICollectionView) is a UIScrollView subclass that generalizes the notion of a UITableView. It has many similarities to a table view; indeed, knowing about table views, you know a great deal about collection views already:
registerClass:forCellWithReuseIdentifier:
or registerNib:forCellWithReuseIdentifier:
. Alternatively, if you’ve started with a UICollectionViewController in a storyboard, just assign the reuse identifier in the storyboard.
Like a UITableView, a collection view has a data source (UICollectionViewDataSource) and a delegate (UICollectionViewDelegate), and it’s going to ask the data source Three Big Questions:
numberOfSectionsInCollectionView:
collectionView:numberOfItemsInSection:
collectionView:cellForItemAtIndexPath:
dequeueReusableCellWithReuseIdentifier:forIndexPath:
.
Like a UITableView, a collection view can clump its data into sections, identified by section number. A section can have a header and footer, though the collection view itself does not call them that; instead, it generalizes its subview types into cells, on the one hand, and supplementary views, on the other. A supplementary view is just a UICollectionReusableView, which happens to be UICollectionViewCell’s superclass. A supplementary view is associated with a kind, which is just an NSString identifying its type; thus you can have a header as one kind, a footer as another kind, and anything else you can imagine.
As with a UITableView, you make supplementary views reusable by registering a class with the collection view, calling registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
(or by entering a reuse identifier in a storyboard). The data source method where you are asked for a supplementary view will be collectionView:viewForSupplementaryElementOfKind:atIndexPath:
. In it, you’ll call dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:
. (The use of an indexPath:
instead of a pure section number is surprising; you’ll typically be interested only in the NSIndexPath’s section
.)
The big difference between a table view and a collection view is how the collection view lays out its elements (cells and supplementary views). A table view lays out its cells in just one way: a vertically scrolling column, where the cells are the width of the table view, the height dictated by the table view or the delegate, and touching one another. A collection view doesn’t do that. In fact, a collection view doesn’t lay out its elements at all! That job is left to another class, a subclass of UICollectionViewLayout.
A UICollectionViewLayout subclass instance is responsible for the overall layout of the collection view that owns it. It does this by answering some Big Questions of its own, posed by the collection view; the most important are these:
collectionViewContentSize
layoutAttributesForElementsInRect:
To answer these questions, the collection view layout needs to ask the collection view some questions of its own, such as numberOfSections
and numberOfItemsInSection:
. (The collection view, in turn, gets the answers to those questions from its data source.)
The collection view layout can thus assign the elements any positions it likes, and the collection view will faithfully draw them in those positions within its content rectangle. The elements are actually of three kinds:
collectionView:cellForItemAtIndexPath:
.
@"Header"
and @"Footer"
. The collection view learns the position of each supplementary view from its layout, and obtains the actual UICollectionReusableView objects from its data source by calling collectionView:viewForSupplementaryElementOfKind:atIndexPath:
.
The collection view layout can position its elements wherever it likes. That seems very open-ended, and indeed it is. To get you started, there’s a built-in UICollectionViewLayout subclass — UICollectionViewFlowLayout.
UICollectionViewFlowLayout arranges its cells in something like a grid. The grid can be scrolled either horizontally or vertically, so this grid is a series of rows or columns. Through properties and a delegate protocol of its own (UICollectionViewDelegateFlowLayout), the UICollectionViewFlowLayout instance lets you provide hints about how big the cells are and how they should be spaced out. It defines two supplementary view types, using them to let you give each section a header and a footer.
Figure 8-11 shows a collection view, laid out with a flow layout, from my Latin flashcard app. This interface simply lists the chapters and lessons into which the flashcards themselves are divided, and allows the user to jump to a desired lesson by tapping it. Previously, I was using a table view to present this list; when collection views were introduced (in iOS 6), I adopted one for this interface, and you can see why. Instead of a lesson item like “1a” occupying an entire row that stretches the whole width of a table, it’s just a little rectangle; in landscape orientation, the flow layout fits five of these rectangles onto a line for me. So a collection view is a much more compact and appropriate way to present this interface than a table view.
If UICollectionViewFlowLayout doesn’t quite meet your needs, you can subclass it, or you can subclass UICollectionViewLayout itself. I’ll talk more about that later on.
Here are the main classes associated with UICollectionView. This is just a conceptual overview; I don’t recite all the properties and methods of each class, which you can learn from the documentation:
A UIViewController subclass. Like a table view controller, UICollectionViewController is convenient if a UICollectionView is to be a view controller’s view, but is not required. It is the delegate and data source of its collectionView
by default. The initializer, if you create one in code, requires you to supply a layout instance for the collection view’s designated initializer:
RootViewController* rvc = [[RootViewController alloc] initWithCollectionViewLayout:[UICollectionViewFlowLayout new]];
Alternatively, there is a Collection View Controller nib object.
A UIScrollView subclass. It has a backgroundColor
(because it’s a view) and optionally a backgroundView
in front of that. Its designated initializer requires you to supply a layout instance, which will be its collectionViewLayout
. Alternatively, there is a Collection View nib object, which comes with a Collection View Flow Layout by default; you can change the collection view layout class with the Layout pop-up menu in the Collection View’s Attributes inspector.
A collection view’s methods are very much parallel to those of a UITableView, only fewer and simpler:
item
property instead of its row
property.
beginUpdates
and endUpdates
, a collection view uses performBatchUpdates:completion:
, which takes blocks.
Having made those mental adjustments, you can guess correctly all the methods of a UICollectionView, except for a couple whose names begin with layoutAttributes...
. To understand what they do, you need to know about UICollectionViewLayoutAttributes.
indexPath
) and the specifications for how and where it should be drawn — specifications that are remarkably reminiscent of view or layer properties, with names like frame
, center
, size
, transform
, and so forth. Layout attributes objects function as the mediators between the layout and the collection view; they are what the layout passes to the collection view to tell it where all the elements of the view should go.
An extremely minimal view class. It has a highlighted
property and a selected
property. It has a contentView
, a selectedBackgroundView
, a backgroundView
, and of course (since it’s a view) a backgroundColor
, layered in that order, just like a table view cell; everything else is up to you.
If you start with a collection view controller in a storyboard, you get prototype cells, just like a table view controller, which you obtain by dequeuing. Otherwise, you obtain cells through registration and dequeuing. This is all exactly parallel to UITableView.
The layout workhorse class for a collection view. A collection view cannot exist without a layout instance! As I’ve already said, the layout knows how much room all the subviews occupy, and supplies the collectionViewContentSize
that sets the contentSize
of the collection view, qua scroll view. In addition, the layout must answer questions from the collection view, by supplying a UICollectionViewLayoutAttributes object, or an NSArray of such objects, saying where and how elements should be drawn. These questions come in two categories:
alpha
is 0
after removal, the element will appear to fade away as it is removed.
The collection view also notifies the layout of pending changes through some methods whose names start with prepare
and finalize
. This is another way for the layout to participate in animations, or to perform other kinds of preparation and cleanup.
UICollectionViewLayout is an abstract class; to use it, you must subclass it, or start with the built-in subclass, UICollectionViewFlowLayout.
A concrete subclass of UICollectionViewLayout; you can use it as is, or you can subclass it. It lays out items in a grid that can be scrolled either horizontally or vertically, and it defines two supplementary element types to serve as the header and footer of a section. A collection view in the nib editor has a Layout pop-up menu that lets you choose a Flow layout, and you can configure the flow layout in the Size inspector; in a storyboard, you can even add and design a header and a footer.
A flow layout has a scroll direction, a sectionInset
(the margins for a section), an itemSize
along with a minimumInteritemSpacing
and minimumLineSpacing
, and a headerReferenceSize
and footerReferenceSize
. That’s all! At a minimum, if you want to see any section headers, you must assign the flow layout a headerReferenceSize
, because the default is {0,0}
. Otherwise, you get initial defaults that will at least allow you to see something immediately, such as an itemSize
of {50,50}
and reasonable default spacing between items and lines.
UICollectionViewFlowLayout also defines a delegate protocol, UICollectionViewDelegateFlowLayout. The flow layout automatically treats the collection view’s delegate as its own delegate. The section margins, item size, item spacing, line spacing, and header and footer size can be set individually through this delegate.
To show that using a collection view is easy, here’s how the view shown in Figure 8-11 is created. I have a UICollectionViewController subclass, LessonListController. Every collection view must have a layout, so LessonListController’s designated initializer initializes itself with a UICollectionViewFlowLayout:
- (id) initWithTerms: (NSArray*) data { UICollectionViewFlowLayout* layout = [UICollectionViewFlowLayout new]; self = [super initWithCollectionViewLayout:layout]; if (self) { // ... perform other self-initializations here ... } return self; }
In viewDidLoad
, we give the flow layout its hints about the sizes of the margins, cells, and headers, as well as registering for cell and header reusability:
- (void)viewDidLoad { [super viewDidLoad]; UICollectionViewFlowLayout* layout = (id)self.collectionView.collectionViewLayout; layout.sectionInset = UIEdgeInsetsMake(10, 20, 10, 20); layout.headerReferenceSize = CGSizeMake(0,40); // only height matters layout.itemSize = CGSizeMake(70,45); [self.collectionView registerNib:[UINib nibWithNibName:@"LessonCell" bundle:nil] forCellWithReuseIdentifier:@"LessonCell"]; [self.collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"LessonHeader"]; self.collectionView.backgroundColor = [UIColor myGolden]; // ... }
The first two of the Three Big Questions to the data source are boring and familiar:
-(NSInteger)numberOfSectionsInCollectionView: (UICollectionView *)collectionView { return [self.sectionNames count]; } -(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [self.sectionData[section] count]; }
The third of the Three Big Questions to the data source creates and configures the cells. In a .xib file, I’ve designed the cell with a single subview, a UILabel with tag 1
; if the text of that label is still @"Label"
, this is a sign that the cell has come freshly minted from the nib and needs further initial configuration. Among other things, I assign each new cell a selectedBackgroundView
and give the label a highlightedTextColor
, to get an automatic indication of selection:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"LessonCell" forIndexPath:indexPath]; UILabel* lab = (UILabel*)[cell viewWithTag:1]; if ([lab.text isEqualToString:@"Label"]) { lab.highlightedTextColor = [UIColor whiteColor]; cell.backgroundColor = [UIColor myPaler]; cell.layer.borderColor = [UIColor brownColor].CGColor; cell.layer.borderWidth = 5; cell.layer.cornerRadius = 5; UIView* v = [UIView new]; v.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.8]; cell.selectedBackgroundView = v; } Term* term = self.sectionData[indexPath.section][indexPath.item]; lab.text = term.lesson; return cell; }
The fourth data source method asks for the supplementary element views; in my case, these are the section headers. I haven’t bothered to design the header in a nib; instead, I configure the entire thing in code. Again I distinguish between newly minted views and reused views; the latter will already have a single subview, a UILabel:
-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { UICollectionReusableView* v = [collectionView dequeueReusableSupplementaryViewOfKind: UICollectionElementKindSectionHeader withReuseIdentifier:@"LessonHeader" forIndexPath:indexPath]; if ([v.subviews count] == 0) { // no label? make one, configure UILabel* lab = [[UILabel alloc] initWithFrame:CGRectMake(10,0,100,40)]; lab.font = [UIFont fontWithName:@"GillSans-Bold" size:20]; lab.backgroundColor = [UIColor clearColor]; [v addSubview:lab]; v.backgroundColor = [UIColor blackColor]; lab.textColor = [UIColor myPaler]; } UILabel* lab = (UILabel*)v.subviews[0]; lab.text = self.sectionNames[indexPath.section]; return v; }
As you can see from Figure 8-11, the first section is treated specially — it has no header, and its cell is wider. I take care of that with two UICollectionViewDelegateFlowLayout methods:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { CGSize sz = ((UICollectionViewFlowLayout*)collectionViewLayout).itemSize; if (indexPath.section == 0) sz.width = 150; return sz; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { CGSize sz = [(UICollectionViewFlowLayout*)collectionViewLayout) headerReferenceSize]; if (section == 0) sz.height = 0; return sz; }
When the user taps a cell, I hear about it through the delegate method collectionView:didSelectItemAtIndexPath:
and respond accordingly. That is the entire code for managing this collection view!
Here’s an example of deleting cells in a collection view. Let’s assume that the cells to be deleted have been selected, with multiple selection being possible. If there are selected cells, they are provided as an array of NSIndexPaths. My data model is once again the usual pair of parallel arrays, a mutable array of strings (sectionNames
) and a mutable array of arrays (sectionData
); each NSIndexPath gets me directly to the corresponding piece of data in sectionData
, so I delete each piece of data in reverse order, keeping track of any arrays (sections) that end up empty. Finally, I delete the items from the collection view, and then do the same for the sections:
- (void) doDelete:(id)sender { // delete selected NSArray* arr = [self.collectionView indexPathsForSelectedItems]; if (!arr || ![arr count]) return; // sort, reverse, delete items from model, keep track of empty sections arr = [arr sortedArrayUsingSelector:@selector(compare:)]; NSMutableIndexSet* empties = [NSMutableIndexSet indexSet]; for (NSIndexPath* ip in [arr reverseObjectEnumerator]) { [self.sectionData[ip.section] removeObjectAtIndex:ip.item]; if (![self.sectionData[ip.section] count]) [empties addIndex:ip.section]; } // delete items from view [self.collectionView performBatchUpdates:^{ [self.collectionView deleteItemsAtIndexPaths:arr]; } completion:^(BOOL finished) { // delete sections from model and then from view if ([empties count]) { [self.sectionNames removeObjectsAtIndexes:empties]; [self.sectionData removeObjectsAtIndexes:empties]; [self.collectionView deleteSections:empties]; } }]; }
Menu handling is also completely parallel to a table view; if you want additional menu items beyond the standard Copy, Cut, and Paste, the corresponding custom selectors must be implemented in a UICollectionViewCell subclass.
To explore what’s involved in writing your own layout class, let’s introduce a simple modification of UICollectionViewFlowLayout.
By default, the flow layout wants to full-justify every row of cells horizontally, spacing the cells evenly between the left and right margins, except for the last row, which is left-aligned. Let’s say that this isn’t what you want — you’d rather that every row be left-aligned, with every cell as far to the left as possible given the size of the preceding cell and the minimum spacing between cells.
To achieve this, you’ll need to subclass UICollectionViewFlowLayout and override two methods, layoutAttributesForElementsInRect:
and layoutAttributesForItemAtIndexPath:
. Fortunately, we’re starting with a layout, UICollectionViewFlowLayout, whose answers to these questions are almost right. So we call super
and make modifications as necessary.
The really important method here is layoutAttributesForItemAtIndexPath:
, which returns a single UICollectionViewLayoutAttributes object.
If the index path’s item
is 0
, we have a degenerate case: the answer we got from super
is right. Alternatively, if this cell is at the start of a row — we can find this out by asking whether the left edge of its frame is close to the margin — we have another degenerate case: the answer we got from super
is right.
Otherwise, where this cell goes depends on where the previous cell goes, so we obtain the frame of the previous cell recursively; we propose to position our left edge a minimal spacing amount from the right edge of the previous cell. We do that by changing the frame
of the UICollectionViewLayoutAttributes object. Then we return that object:
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath: (NSIndexPath *)indexPath { UICollectionViewLayoutAttributes* atts = [super layoutAttributesForItemAtIndexPath:indexPath]; if (indexPath.item == 0) return atts; if (atts.frame.origin.x - 1 <= self.sectionInset.left) return atts; NSIndexPath* ipPrev = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section]; CGRect fPrev = [self layoutAttributesForItemAtIndexPath:ipPrev].frame; CGFloat rightPrev = fPrev.origin.x + fPrev.size.width + self.minimumInteritemSpacing; CGRect f = atts.frame; f.origin.x = rightPrev; atts.frame = f; return atts; }
The other method, layoutAttributesForElementsInRect:
, returns an NSArray of UICollectionViewLayoutAttributes objects for all the cells and supplementary views in a rect. Again we call super
and modify the resulting array so that if an element is a cell, its UICollectionViewLayoutAttributes is the result of our layoutAttributesForItemAtIndexPath:
:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray* arr = [super layoutAttributesForElementsInRect:rect]; for (UICollectionViewLayoutAttributes* atts in arr) { if (nil == atts.representedElementKind) { // it's a cell NSIndexPath* ip = atts.indexPath; atts.frame = [self layoutAttributesForItemAtIndexPath:ip].frame; } } return arr; }
Apple supplies some further interesting examples of subclassing UICollectionViewFlowLayout. For instance, the LineLayout example (accompanying the WWDC 2012 videos) implements a single row of horizontally scrolling cells, where a cell grows as it approaches the center of the screen and shrinks as it moves away. To do this, it first of all overrides a UICollectionViewLayout method I didn’t mention earlier, shouldInvalidateLayoutForBoundsChange:
; this causes layout to happen repeatedly while the collection view is scrolled. It then overrides layoutAttributesForElementsInRect:
to do the same sort of thing I did a moment ago: it calls super
and then modifies, as needed, the transform3D
property of the UICollectionViewLayoutAttributes for the onscreen cells.
(It also overrides another UICollectionViewLayout method I didn’t mention, targetContentOffsetForProposedContentOffset:withScrollingVelocity:
, which is like UIScrollViewDelegate’s scrollViewWillEndDragging:withVelocity:targetContentOffset:
. This is just a nice touch so that when the user scrolls, a cell always ends up exactly centered on the screen.)
You can also subclass UICollectionViewLayout itself. The WWDC 2012 videos demonstrate a UICollectionViewLayout subclass that arranges its cells in a circle; the WWDC 2013 videos demonstrate a UICollectionViewLayout subclass that piles its cells into a single stack in the center of the collection view, like a deck of cards seen from above.
A collection view layout can be powerful and complex, but getting started writing one from scratch is not difficult. To illustrate, I’ll write a collection view layout that ignores sections and presents all cells as a simple grid of squares.
In my UICollectionViewLayout subclass, called MyLayout, the really big questions I need to answer are collectionViewContentSize
and layoutAttributesForElementsInRect:
. To answer them, I’ll calculate the entire layout of my grid beforehand. The prepareLayout
method is the perfect place to do this; it is called every time something about the collection view or its data changes. I’ll calculate the grid of cells and express their positions as an array of UICollectionViewLayoutAttributes objects; I’ll store that array in an instance variable _atts
, and I’ll store the size of the grid in an instance variable _sz
:
-(void)prepareLayout { // how many items are there in total? int total = 0; NSInteger sections = [self.collectionView numberOfSections]; for (int i = 0; i < sections; i++) total += [self.collectionView numberOfItemsInSection:i]; // work out cell size based on bounds width CGSize sz = self.collectionView.bounds.size; CGFloat width = sz.width; int shortside = floor(width/50.0); CGFloat cellside = width/(float)shortside; // generate attributes for all cells int x = 0; int y = 0; NSMutableArray* atts = [NSMutableArray new]; for (int i = 0; i < sections; i++) { int jj = [self.collectionView numberOfItemsInSection:i]; for (int j = 0; j < jj; j++) { UICollectionViewLayoutAttributes* att = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: [NSIndexPath indexPathForItem:j inSection:i]]; att.frame = CGRectMake(x*cellside,y*cellside,cellside,cellside); [atts addObject:att]; x++; if (x >= shortside) { x = 0; y++; } } } self->_atts = atts; // generate overall grid size int fluff = (x == 0) ? 0 : 1; self->_sz = CGSizeMake(width, (y+fluff) * cellside); }
collectionViewContentSize
and layoutAttributesForElementsInRect:
are obvious: I’ll just return the _sz
or _atts
instance variable, respectively. I’m ignoring the rect:
parameter in layoutAttributesForElementsInRect:
, as there is no efficiency to be gained by limiting myself to it; I have the entire array of UICollectionViewLayoutAttributes objects ready, so it is simplest and quickest to provide a pointer to it:
- (CGSize)collectionViewContentSize { return self->_sz; } - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { return self->_atts; }
layoutAttributesForItemAtIndexPath:
is implemented by looking up the corresponding value in my _atts
array:
- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath: (NSIndexPath *)indexPath { for (UICollectionViewLayoutAttributes* att in self->_atts) { if ([att.indexPath isEqual:indexPath]) return att; } return nil; // shouldn't happen }
Finally, I implement shouldInvalidateLayoutForBoundsChange:
to return YES, so that if the interface is rotated, my prepareLayout
will be called again to recalculate the grid. There’s a potential source of inefficiency here: the user scrolling the collection view counts as a bounds change as well. Therefore I return NO unless the bounds width has changed:
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { return newBounds.size.width != self->_sz.width; }
An astonishing and delightful feature of a collection view is that its layout object can be swapped out on the fly. You can substitute one layout for another, by calling setCollectionViewLayout:animated:completion:
. The data hasn’t changed, and the collection view can identify each element uniquely and persistently, so it responds by moving every element from its position according to the old layout to its position according to the new layout — and, if the animated:
argument is YES, it does this with animation! Thus the elements are seen to rearrange themselves, as if by magic.
New in iOS 7, this animated change of layout can be driven interactively (in response, for example, to a user gesture; compare Chapter 6 on interactive transitions). You call startInteractiveTransitionToCollectionViewLayout:completion:
on the collection view, and a special layout object is returned — a UICollectionViewTransitionLayout instance (or a subclass thereof; to make it a subclass, you need to have implemented collectionView:transitionLayoutForOldLayout:newLayout:
in your collection view delegate). This transition layout is temporarily made the collection view’s layout, and your job is then to keep it apprised of the transition’s progress (through its transitionProgress
property) and ultimately to call finishInteractiveTransition
or cancelInteractiveTransition
on the collection view.
Furthermore, also new in iOS 7, when one collection view controller is pushed on top of another in a navigation interface, the runtime will do exactly the same thing for you, as a custom view controller transition (again, compare Chapter 6). To arrange this, the first collection view controller’s useLayoutToLayoutNavigationTransitions
property must be NO and the second collection view controller’s useLayoutToLayoutNavigationTransitions
property must be YES. The result is that when the second collection view controller is pushed onto the navigation controller, the collection view remains in place, and the layout specified by the second collection view controller is substituted for the collection view’s existing layout, with animation.
The effect, as the second collection view controller is pushed onto the navigation stack, is conceptually rather unsettling. Although there are two collection view controllers, and although the second view controller has a view (the collection view), and its viewDidLoad
and viewWillAppear:
(as well as the first view controller’s viewWillDisappear:
) are called as you would expect, the same collection view is also still the first view controller’s view, and the collection view’s data source and delegate are still the first view controller. Later, after the transition is complete, the collection view’s delegate becomes the second view controller, but its data source is still the first view controller. I find this profoundly weird.
The UICollectionViewLayoutAttributes class adopts the UIDynamicItem protocol (see Chapter 4). Thus, collection view elements can be animated under UIKit dynamics. The world of the animator here is not a superview but the layout itself; instead of initWithReferenceView:
, you’ll create the UIDynamicAnimator with initWithCollectionViewLayout:
. The layout’s collectionViewContentSize
determines the bounds of this world. Convenience methods are provided so that your code can access an animated collection view item’s layout attributes directly from the animator.
You’ll need a custom collection view layout subclass, because otherwise you won’t be able to see any animation. On every frame of its animation, the UIDynamicAnimator is going to change the layout attributes of some items, but the collection view is still going to draw those items in accordance with the layout’s layoutAttributesForElementsInRect:
. The simplest solution is to override layoutAttributesForElementsInRect:
so as to obtain those layout attributes from the UIDynamicAnimator. This cooperation will be easiest if the layout itself owns and configures the animator.
In this example, we’re in the layout subclass, setting up the animation. The layout subclass has a property to hold the animator, as well as a BOOL property to signal when an animation is in progress:
UIDynamicAnimator* anim = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self]; self.animator = anim; self.animating = YES; // ... configure rest of animation ...
Our implementation of layoutAttributesForElementsInRect:
, if we are animating, substitutes the layout attributes that come from the animator for those we would normally return; the technique I use here relies on the fact that the animator convenience methods layoutAttributesForCellAtIndexPath:
and so forth return nil if the specified item is not being animated. In this particular example, both cells and supplementary items (headers and footers) can be animated, so the two cases have to be distinguished:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray* arr = [super layoutAttributesForElementsInRect:rect]; if (self.animating) { NSMutableArray* marr = [NSMutableArray new]; for (UICollectionViewLayoutAttributes* atts in arr) { NSIndexPath* path = atts.indexPath; UICollectionViewLayoutAttributes* atts2 = nil; switch (atts.representedElementCategory) { case UICollectionElementCategoryCell: { atts2 = [self.animator layoutAttributesForCellAtIndexPath:path]; break; } case UICollectionElementCategorySupplementaryView: { NSString* kind = atts.representedElementKind; atts2 = [self.animator layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:path]; break; } default: break; } [marr addObject: (atts2 ? atts2 : atts)]; } return marr; } return arr; }