Though we can do a lot with table views, quite often, we may want to format the data for each row in ways that UItableViewCell does not support. In those cases, we have two options:
In this section, we are going to create a new application with another table view. In each row, we have two lines of information along with two labels. This application will display the country and group names of 32 countries for World Cup 2010, South Africa.
We will see that adding subviews to the table view cell can give us multiline rows.
Let's create a new project using view-based application template with a name "Cells".
Then, add a Table View to the main view. Set table view's delegate and
datasource to File's Owner.
Save the nib and back to Xcode.
Let's look at the "CellsViewController.h"
#import <UIKit/UIKit.h> #define kNameValueTag 1 #define kGroupValueTag 2 @interface CellsViewController : UIViewController{ NSArray *computers; } @property (nonatomic, retain) NSArray *computers; @end
We are going to add four subviews to the table view cell, and two of those need to be changed for every row. In order to do that, we need a mechanism that allows us to retrieve the two fields from the cell when we update that cell with a particular data of the row.
In CellsViewController.m file, we should set up some data to use, and then implement the table datasource method to feed that data to the table.
#import "CellsViewController.h" @implementation CellsViewController @synthesize computers; - (void)viewDidLoad { NSDictionary *row1 = [[NSDictionary alloc] initWithObjectsAndKeys: @"South Africa", @"Name", @"A", @"Group", nil]; NSDictionary *row2 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Mexico", @"Name", @"A", @"Group", nil]; NSDictionary *row3 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Uruguay", @"Name", @"A", @"Group", nil]; NSDictionary *row4 = [[NSDictionary alloc] initWithObjectsAndKeys: @"France", @"Name", @"A", @"Group", nil]; NSDictionary *row5 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Argentina", @"Name", @"B", @"Group", nil]; NSDictionary *row6 = [[NSDictionary alloc] initWithObjectsAndKeys: @"South Korea", @"Name", @"B", @"Group", nil]; NSDictionary *row7 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Nigeria", @"Name", @"B", @"Group", nil]; NSDictionary *row8 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Greece", @"Name", @"B", @"Group", nil]; NSDictionary *row9 = [[NSDictionary alloc] initWithObjectsAndKeys: @"England", @"Name", @"C", @"Group", nil]; NSDictionary *row10 = [[NSDictionary alloc] initWithObjectsAndKeys: @"United States", @"Name", @"C", @"Group", nil]; NSDictionary *row11 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Algeria", @"Name", @"C", @"Group", nil]; NSDictionary *row12 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Slovenia", @"Name", @"C", @"Group", nil]; NSDictionary *row13 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Germany", @"Name", @"D", @"Group", nil]; NSDictionary *row14 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Australia", @"Name", @"D", @"Group", nil]; NSArray *array = [[NSArray alloc] initWithObjects: row1, row2, row3, row4, row5, row6, row7, row8, row9, row10, row11, row12, row13, row14, nil]; self.computers = array; [row1 release]; [row2 release]; [row3 release]; [row4 release]; [row5 release]; [row6 release]; [row7 release]; [row8 release]; [row9 release]; [row10 release]; [row11 release]; [row12 release]; [row13 release]; [row14 release]; [array release]; } - (void)didReceiveMemoryWarning { // Releases the view if it doesn't have a superview. [super didReceiveMemoryWarning]; // Release any cached data, images, etc that aren't in use. } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; self.computers = nil; } - (void)dealloc { [computers release]; [super dealloc]; } #pragma mark - #pragma mark Table Data Source Methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.computers count]; } -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellTableIdentifier = @"CellTableIdentifier "; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: CellTableIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier: CellTableIdentifier] autorelease]; CGRect nameLabelRect = CGRectMake(0, 5, 70, 20); UILabel *nameLabel = [[UILabel alloc] initWithFrame:nameLabelRect]; nameLabel.textAlignment = UITextAlignmentRight; nameLabel.text = @"Name:"; nameLabel.font = [UIFont boldSystemFontOfSize:12]; [cell.contentView addSubview: nameLabel]; [nameLabel release]; CGRect groupLabelRect = CGRectMake(0,30, 70, 20); UILabel *groupLabel = [[UILabel alloc] initWithFrame: groupLabelRect]; groupLabel.textAlignment = UITextAlignmentRight; groupLabel.text = @"Group:"; groupLabel.font = [UIFont boldSystemFontOfSize:12]; [cell.contentView addSubview: groupLabel]; [groupLabel release]; CGRect nameValueRect = CGRectMake(80, 5, 200, 20); UILabel *nameValue = [[UILabel alloc] initWithFrame: nameValueRect]; nameValue.tag = kNameValueTag; [cell.contentView addSubview:nameValue]; [nameValue release]; CGRect groupValueRect = CGRectMake(80, 30, 200, 20); UILabel *groupValue = [[UILabel alloc] initWithFrame: groupValueRect]; groupValue.tag = kGroupValueTag; [cell.contentView addSubview:groupValue]; [groupValue release]; } NSUInteger row = [indexPath row]; NSDictionary *rowData = [self.computers objectAtIndex:row]; UILabel *name = (UILabel *)[cell.contentView viewWithTag: kNameValueTag]; name.text = [rowData objectForKey:@"Name"]; UILabel *group = (UILabel *)[cell.contentView viewWithTag: kGroupValueTag]; group.text = [rowData objectForKey:@"Group"]; return cell; } @end
The method, viewDidLoad, creates dictionaries. Each dictionary contains the name and group information for one row in the table. The key for the row of name is Name, and the key for the row of group is Group. All the dictionaries is a single array in our application.
Let's look at tableView:cellForRowWithIndexPath: method.
This is where we are getting into some new stuff. The first two lines
create an identifier and ask the table to dequeue a table view cell if it
has one.
We should create a new cell if the table does not have any cells available.
We also need to create and add the subviews that we will be using to implement
out two-line-per-row table.
Let's look at the code. First, we create a cell and specify the default style.
The style won't matter since we will add our own subviews to display our data
instead of using the ones provided.
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier: CellTableIdentifier] autorelease];
Then, we create four UILabels and add them to the table view cell. The table view cell already has a UIView called contentView, which it uses to group all of its subviews. So, we don't add the labels as subviews directly to the table view cell but rather to its content view.
[cell.contentView addSubview: nameLabel];Tow of these labels contain static text. The label nameLabel contains the text Name: and the label groupLabel contains the text Group:. Those are just static labels that we won't change. The other two labels, however, will be used to display our row0specific data. We assign values to both of them to retrieve these fields later on. For instance, we assign the constant kNameValueTag into nameValue's tag field:
nameValue.tag = kNameValueTag;
Once, we've created our new cell, we use the indexPath argument that was passed in to determine which row the table is requesting a cell for and then use that row value to grab the correct dictionary for the requested row.
NSUInteger row = [indexPath row]; NSDictionary *rowData = [self.computers objectAtIndex:row];
Here, we use the tags we set before to retrieve the label whose value we need to set.
UILabel *name = (UILabel *)[cell.contentView viewWithTag: kNameValueTag];
Once we have that label, we just set its text to one of the values we pull from the dictionary that represents this row.
name.text = [rowData objectForKey:@"Name"];
Build and Run.
Being able to add views to the table view gives us more flexibility than using the standard view cell alone. But positioning and adding all the subviews programmatically are may not be the best way of making the table view cell.
We are going to recreate the same two-lined table using Interface Builder by creating a subclass of UITableViewCell and a new nib file. The new nib file will contain the table view cell. After that, when we need a table view cell to represent a row, we will just load in our subclass from the nib file instead of adding subviews to a standard view cell. And we all also use two outlets we will add to set the name and group.
Let's create a new subclass and name it CustonCell.m.
From the
Cocoa Touch Class and select Objective-C class.
Then, select UITableViewCell subclass and name it CustomCell.m when
the new window pops up.
From the Resources folder in Xcode, select Add->New->File... From the new
file assitant, choose User Interface and select Empty XIB. Name it
CustomCell.xib.
We will use outlets in our subclass to set the value that should be changed for each row. By creating a subclass we can make our code much more clean because we will be able to set the labels on each row's cell just by setting properties. For instance:
cell.nameLable = @"EasyToLabel";
New that we have all the pieces we need to move on, let's modify "CustonCell.h".
#import <UIKit/UIKit.h> @interface CustomCell : UITableViewCell { UILabel *nameLabel; UILabel *groupLabel; } @property (nonatomic, retain) IBOutlet UILabel *nameLabel; @property (nonatomic, retain) IBOutlet UILabel *groupLabel; @end
Then, move on to implementation file, "CustomCell.m".
#import "CustomCell.h" @implementation CustomCell @synthesize nameLabel; @synthesize groupLabel; - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } - (void)dealloc { [nameLabel release]; [groupLabel release]; [super dealloc]; } @end
We are about to design the table view cell in Interface Builder.
Make sure the two files, "CustomCell.m" and "CustomCell.h", saved.
Let's open Interface Builder by double-clicking "CustomCell.xib" file.
We have only two icons in this nib's main window: File's Owner and
First Responder.
Drag Table View Cell to the nib's main window.
Bring up the identity inspector and change the class to CustomCell from UITableViewCell and bring up size inspector to change cell height to 65 from 44 to have more room to play with.
Even though UITableViewCell is a subclass of UIView, it uses a content view to hold and group its subviews. Open up Custom Cell Content View window by double-clicking Custom Cell icon. Then, you will see a gray dashed rounded rectangle labeled Content View. This indicates that you should add a View from the library. So, drag a View into the Custom Cell window. But when you release the view, it may be the wrong size for our window. So, with the new view selected, bring up size inspector. Change View's size and position to fit the Custom Cell. Set x to 0, y to 0, w to 320, and h to 65.
While Custom Cell icon selected, bring up attribute inspector. Set the field labeled Identifier to CustomCellIdentifier.
Now we have a canvas to design our table view cell in Interface Builder. Drag four labels over from the library to the Custom Cell window. Rename them as in the picture below.
Control-drag from the Custom Cell icon to the label on the view, assign it to the outlet nameLabel.
Then to the other do the same and assign it to the groupLabel outlet.
We need to make some changes to the tableView:cellForRowAtIndexPath: method to use the cell we designed. Here is the new version of the method.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CustomCellIdentifier = @"CustomCellIdentifier "; CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier]; if (cell == nil) { NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil]; for (id oneObject in nib) if ([oneObject isKindOfClass:[CustomCell class]]) cell = (CustomCell *)oneObject; } NSUInteger row = [indexPath row]; NSDictionary *rowData = [self.computers objectAtIndex:row]; cell.groupLabel.text = [rowData objectForKey:@"Group"]; cell.nameLabel.text = [rowData objectForKey:@"Name"]; return cell; }
We need to add our new header to "CellsViewController.m"
#import "CustomCell.h"
Since we made the table view cell in a nib file, if there are no reusable cells, we load the one from the nib. When loading the nib, we get an array that contains all the objects in the nib. We will loop through all the objects in the nib and look for an instance of the CustomCell class.
Because we change the height of our table view cell from the default value, there is one other addition we should make. Otherwise, cell may not displayed properly. So, let's add the following delegate method to "CellViewController.m".
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return kTableViewRowHeight; }
But we can't get this value from the cell because this delegate method may be called before the cell exists. So, we should make it hard coded as constant, and we no longer need the tag constants. Here is the new "CellsViewController.h"
#import <UIKit/UIKit.h> #define kTableViewRowHeight 66#define kNameValueTag 1#define kGroupValueTag 2@interface CellsViewController : UIViewController{ NSArray *computers; } @property (nonatomic, retain) NSArray *computers; @end
Here is the final version of "CellsViewController.m".
#import "CellsViewController.h" #import "CustomCell.h" @implementation CellsViewController @synthesize computers; - (void)viewDidLoad { NSDictionary *row1 = [[NSDictionary alloc] initWithObjectsAndKeys: @"South Africa", @"Name", @"A", @"Group", nil]; NSDictionary *row2 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Mexico", @"Name", @"A", @"Group", nil]; NSDictionary *row3 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Uruguay", @"Name", @"A", @"Group", nil]; NSDictionary *row4 = [[NSDictionary alloc] initWithObjectsAndKeys: @"France", @"Name", @"A", @"Group", nil]; NSDictionary *row5 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Argentina", @"Name", @"B", @"Group", nil]; NSDictionary *row6 = [[NSDictionary alloc] initWithObjectsAndKeys: @"South Korea", @"Name", @"B", @"Group", nil]; NSDictionary *row7 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Nigeria", @"Name", @"B", @"Group", nil]; NSDictionary *row8 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Greece", @"Name", @"B", @"Group", nil]; NSDictionary *row9 = [[NSDictionary alloc] initWithObjectsAndKeys: @"England", @"Name", @"C", @"Group", nil]; NSDictionary *row10 = [[NSDictionary alloc] initWithObjectsAndKeys: @"United States", @"Name", @"C", @"Group", nil]; NSDictionary *row11 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Algeria", @"Name", @"C", @"Group", nil]; NSDictionary *row12 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Slovenia", @"Name", @"C", @"Group", nil]; NSDictionary *row13 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Germany", @"Name", @"D", @"Group", nil]; NSDictionary *row14 = [[NSDictionary alloc] initWithObjectsAndKeys: @"Australia", @"Name", @"D", @"Group", nil]; NSArray *array = [[NSArray alloc] initWithObjects: row1, row2, row3, row4, row5, row6, row7, row8, row9, row10, row11, row12, row13, row14, nil]; self.computers = array; [row1 release]; [row2 release]; [row3 release]; [row4 release]; [row5 release]; [row6 release]; [row7 release]; [row8 release]; [row9 release]; [row10 release]; [row11 release]; [row12 release]; [row13 release]; [row14 release]; [array release]; } - (void)didReceiveMemoryWarning { // Releases the view if it doesn't have a superview. [super didReceiveMemoryWarning]; // Release any cached data, images, etc that aren't in use. } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; self.computers = nil; } - (void)dealloc { [computers release]; [super dealloc]; } #pragma mark - #pragma mark Table Data Source Methods - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return kTableViewRowHeight; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.computers count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CustomCellIdentifier = @"CustomCellIdentifier "; CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier]; if (cell == nil) { NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil]; for (id oneObject in nib) if ([oneObject isKindOfClass:[CustomCell class]]) cell = (CustomCell *)oneObject; } NSUInteger row = [indexPath row]; NSDictionary *rowData = [self.computers objectAtIndex:row]; cell.groupLabel.text = [rowData objectForKey:@"Group"]; cell.nameLabel.text = [rowData objectForKey:@"Name"]; return cell; } @end
Build and Run.
This section will show another fundamental aspect of tables. We will still use a single table view with no hierarchies yet, however, we will divide data into sections.
Create a new Xcode using view-based application template. Name it Sections.
Open the Interface Builder and drop a table view onto the View window. Then, connect the dataSource and delegate connections to the File's Owner icon.
While the table view selected, change the table view's Style from Plain to Grouped on the attributes inspector.
Then, we will have a new view. Pictures below show the difference between plain and grouped styles.
Save the nib and back to Xcode.
I made a list with countries that will compete in 2010 World Cup with additional group information. The file is sortednames.plist and added to the project's Resources folder.
Once it's added to the project, single-click sortednames.plist to get the sense of what it looks like. It's a property list that contains a dictionary, with one entry for each group from A to H. Underneath each group is a list of names of the countries for that group. We are going to use the data from this property list to feed the table view, creating a section for each letter.
Let's look at the interface file, "SectionsViewController.h".
#import <UIKit/UIKit.h> @interface SectionsViewController : UIViewController <UITableViewDataSource, UITableViewDelegate> { NSDictionary *names; NSArray *keys; } @property (nonatomic, retain) NSDictionary *names; @property (nonatomic, retain) NSArray *keys; @end
Then, the implementation file, "SectionsViewController.m".
#import "SectionsViewController.h" @implementation SectionsViewController @synthesize names; @synthesize keys; - (void)viewDidLoad { NSString *path = [[NSBundle mainBundle] pathForResource:@"sortednames" ofType:@"plist"]; NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:path]; self.names = dict; [dict release]; NSArray *array = [[names allKeys] sortedArrayUsingSelector: @selector(compare:)]; self.keys = array; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; self.names = nil; self.keys = nil; } - (void)dealloc { [names release]; [keys release]; [super dealloc]; } #pragma mark - #pragma mark Table View Data Source Methods - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [keys count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSString *key = [keys objectAtIndex:section]; NSArray *nameSection = [names objectForKey:key]; return [nameSection count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSUInteger section = [indexPath section]; NSUInteger row = [indexPath row]; NSString *key = [keys objectAtIndex:section]; NSArray *nameSection = [names objectForKey:key]; static NSString *SectionsTableIdentifier = @"SectionsTableIdentifier"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: SectionsTableIdentifier ]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier: SectionsTableIdentifier ] autorelease]; } cell.textLabel.text = [nameSection objectAtIndex:row]; return cell; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *key = [keys objectAtIndex:section]; return key; } - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { return keys; } @end
In the viewDidLoad method, we created an NSDictionary instance from property list. We assign it to names.
self.names = dict;
Then we grabbed all the keys from that dictionary and sorted to make an NSArray.
NSArray *array = [[names allKeys] sortedArrayUsingSelector: @selector(compare:)];
Let's look at the datasource method. The method numberOfSectionsInTableView: specifies the number of sections. It tells the table view that we have one section for each key in our dictionary.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [keys count]; }
Another datasource method, tableView: numberOfRowsInSection:, calculates the number of rows in a specific section. By retrieving the array that corresponds to the section in question, we can get the number of rows for each group. Actually, all sections have 4 rows in our plist, which makes this is not necessary. But we leave it that way just in case.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSString *key = [keys objectAtIndex:section]; NSArray *nameSection = [names objectForKey:key]; return [nameSection count]; }
In tableView: cellForRowAtIndexPath: method, we have to extract both the section and row from the index path and use that to determine which value to use. The section will let us know which array to pull out of the country names dictionary, and then we can use the row to figure out which value from that array to use.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSUInteger section = [indexPath section]; NSUInteger row = [indexPath row]; NSString *key = [keys objectAtIndex:section]; NSArray *nameSection = [names objectForKey:key]; static NSString *SectionsTableIdentifier = @"SectionsTableIdentifier"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: SectionsTableIdentifier ]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier: SectionsTableIdentifier ] autorelease]; } cell.textLabel.text = [nameSection objectAtIndex:row]; return cell; }
The method tableView: titleForHeaderInSection: allows us to specify an optional header value for each section. We just return the letter for this group.
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *key = [keys objectAtIndex:section]; return key; }
Build and Run.
If we use the Plain instead of Grouped for its Style on the attributes inspector, we get this result.
With our data, we do not have any problems. But potentially it has. When we have huge number of rows, it's very hard to scroll through. One solution to this problem is to add an index down the right side of the table view. To our surprise, the feature has already been setup and working. The part of code responsible for this is:
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { return keys; }
In this method, the delegate is asking for an array of the values to display in the index. The returned array must have the same number of entries as you have sections, and the values must correspond to the appropriate section. In other words, the first item in this array will take the user to the first section, which is section 0.
I will come back later for this section.
Previous Sections.