Channels ▼

Embedded Systems

Making Two-Way Tables in iOS

The Chicken and the Egg Problem

The solution seems simple enough: Just write the edited value from the text field back into its proper element in the array. However, the problem becomes apparent after a little thought: How do you know which array and which element to update? UITextField carries a lot of information about itself, but not the section or row of the cell that you tapped on. Nor does the cell have any information about which text field was tapped. How can we find this critical information?

One way to deal with the problem is, when textFieldShouldReturn executes, work your way up the view hierarchy using superview until you get the view class of the cell, and have it obtain the reference to the table. Knowing the cell, you can message the table to get its indexPath. You then use the indexPath to extract the row and column information:

	 UIView *view = (UIView *)[textField superview];
	 UITableViewCell *cell = (UITableViewCell *)[view superview];
	 UITableView *table = (UITableView *)[cell superview];
	 NSIndexPath *textFieldIndexPath = [table indexPathForCell:cell];
	 NSUInteger cellSection = [textFieldIndexPath section];
	 NSUInteger cellRow =[textFieldIndexPath row];

Then you can compare the textField object passed to this method and compare it to our date, cooling, and heating text field objects to find out which object was tapped. However, that is an awful lot of work to get this information. Fortunately, you can use the text field's view tag to expedite the process. When the cell is created or updated, you encode the section, row, and view information into the view's tag. When the textFieldShouldReturn method is invoked, just decode the value in tag of the UITextField's view to get the section and row information.

Let's look at the cell update code in cellForRowAtIndexPath in Listing Six to see how the section, row, and field information are encoded into the tag value:

Listing Six

#define TIME_FIELD    1

	// Configure the cell's contents with regards to section and row
	// Fetch content for row label (day of week)
	NSDictionary *dictionary = [listOfRows objectAtIndex:indexPath.section];
	NSArray *array = [dictionary objectForKey:@"Content"];
	NSString *cellValue = [array objectAtIndex:indexPath.row];
	// Set text in cell label
	cell.textLabel.text = cellValue;
	// Set up time field display. Encode section and row into tag value.
	timeField.tag = (indexPath.section * 100) + (indexPath.row * 10) + TIME_FIELD;
	// Drop in time content from timeOfDays array
	timeField.text = [NSString stringWithFormat:@"%@", [self.dateFormatter stringFromDate:[settings.timeOfDays objectAtIndex:((indexPath.section * 4) + indexPath.row)]]];
	// Set up heating field display's tag. Drop in content from heatStr array.
	heatingField.tag = (indexPath.section * 100) + (indexPath.row * 10) + HEATING_FIELD;
	heatingField.text = [NSString stringWithFormat:@"%@", [settings.heatStr objectAtIndex:((indexPath.section * 4) + indexPath.row)]];
	// Set up tag for cooling field. Insert content from coolStr array.
	coolingField.tag = (indexPath.section * 100) + (indexPath.row * 10) + COOLING_FIELD;
	coolingField.text = [NSString stringWithFormat:@"%@", [settings.coolStr objectAtIndex:((indexPath.section * 4) + indexPath.row)]];	

    return cell;

Here we encode the section information by multiplying its value by 100, while the row value is multiplied by ten, and the field value is just a single digit. These values are summed together to form the tag value.

When textFieldShouldReturn executes now, determining the section, row and field information of the object that was tapped just takes a few compare operations (Listing Seven):

Listing Seven

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
	// Extract section, row, and tag to determine which text field that was tapped
	NSUInteger row = (textField.tag % 100) / 10;
	NSUInteger section = (textField.tag / 100);
	NSUInteger inputFieldTag = (textField.tag - (section * 100) - (row * 10));	
	// Handle specific field selection, based on textField's tag
	if (inputFieldTag == HEATING_FIELD) {
		[settings.heatStr replaceObjectAtIndex:((section * 4) + row) withObject:textField.text];
	if (inputFieldTag == COOLING_FIELD) {
		[settings.coolStr replaceObjectAtIndex:((section * 4) + row) withObject:textField.text];
	if (inputFieldTag == TIME_FIELD) {
		[settings.timeOfDays replaceObjectAtIndex:((section * 4) + row) withObject:[dateFormatter dateFromString:textField.text]];
	// Update row with modified field. First get indexPath to row. Will only update one row.
	NSUInteger fieldRowIndex[] = {section, row};
	NSIndexPath *rowIndexPath = [[NSIndexPath alloc] initWithIndexes:fieldRowIndex length:2];
	NSArray *rowArray = [[NSArray alloc] initWithObjects:rowIndexPath, nil];
	[self.tableView reloadRowsAtIndexPaths:rowArray withRowAnimation:UITableViewRowAnimationNone]; // Do the update.
	// Release objects used t make index path
	[rowArray release];
	[rowIndexPath release];
	// Signal that editing is done, release first responder
	[textField resignFirstResponder];
	return YES;

With the section and row values in hand, it's easy to calculate the array index, then use NSMutableArray's replaceObjectAtIndex method to write the new value into the correct element in the array. The other point of interest is that we use the section and row information to create an indexPath. This indexPath is used to specify the exact row that needs refreshing when invoking reloadRowsAtIndexPaths. For small tables, invoking reloadData to refresh the entire contents of the table is suitable. However, for large, complex tables you should use reloadRowsAtIndexPaths to keep the overhead required to perform the update to a minimum. You shouldn't see the update occur, which is fine: Such operations should be transparent to the user. If you want to confirm that the refresh takes place, modify the animation argument in the reloadRowsAtIndexPaths method and see what happens. This completes the construction of the two-way table. What you do with the updated data in the table's data model is up to you.

Final Touches (No Pun)

When entering text into timeField, if you enter anything other than a time consisting of digits, a colon, and AM or PM, the dateFormatter tosses an exception. You need to add code to do sanity checks on the edited text, or restrict what can be entered.

One way to do this is to use the UITextInputTraits_Protocol's keyboardType property to specify a keypad for numeric input only. This works for entering temperatures, but still does not solve the problem for timeField, because it requires some non-numeric characters. Alternatively, you can use the UITextFieldDelegate_Protocol's textFieldShouldBeginEditing method to present a date picker rather than a keyboard. How to do that is the subject of an entire article in itself.

The iOS interface is a colorful one, and we can leverage that to make the table UI more useful. For example, we can make the heatingField view red and the coolingField view blue to help remind users as to which temperature they are changing.

To do this, go back into IB, open TempCell.xib, and select the heatingField object. Use the Attributes Inspector to change the background color of the view. If you run the app however, all you get is a colored rectangular border around the field. For whatever reason, adding color to a rounded text field is fraught with gotchas. First, to get color within the proper field, adjust the view's alpha channel to 0.75. This value strikes a reasonable balance between color intensity in the view while keeping the displayed text from becoming too faint. You still have that rectangular border, however. To fix this, add the Quartz Core libraries to the app by inserting the following header file into the TableTempControl.m file.

#import <QuartzCore/QuartzCore.h>

We will use the Quartz layer feature to trim the colored corners. Go to the file's cellForRowAtIndexPath method and revise the cell creation code to match that in Listing Eight:

Listing Eight

   if (cell == nil) {
		[[NSBundle mainBundle] loadNibNamed:@"TempCell" owner:self options:nil]; 
		cell = settingsCell; 
		settingsCell = nil;

		cell.textLabel.backgroundColor = [UIColor clearColor];

		timeField.font = [UIFont systemFontOfSize:[UIFont labelFontSize]];	
		heatingField.font = [UIFont systemFontOfSize:[UIFont labelFontSize]];		
		coolingField.font = [UIFont systemFontOfSize:[UIFont labelFontSize]];
		[[heatingField layer] setCornerRadius:8.0f];
		[[heatingField layer] setMasksToBounds:YES];
		[[coolingField layer] setCornerRadius:8.0f];
		[[coolingField layer] setMasksToBounds:YES];

When you build the project and run the app, it should resemble that in Figure 2.

Now that you know what's required to write a two-way table, go forth and write business apps.

Assoicated code and files for this article can be downloaded here.

— Tom Thompson is head of Proactive Support for embedded products at Freescale Semiconductor.

Related Reading

More Insights

Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.