Introduction
Are you satisfied with your approach for supporting dynamically appearing rows in your Cocoa Touch forms like the calendar app’s date pickers? If you’re looking for another one, this article may be the one you’ve been searching for.
The goal of this technique is to eliminate obfuscating conditionals when identifying rows. While testing for cell visibility cannot entirely be eliminated, it can be reduced to essential conditions. The basis for the technique is to use a mapping table to identify the position for all the rows in the form. With this approach, there is no difference between identifying a static row from dynamic ones.
In the remainder of this article, I will walk you through a simple application that accomplishes nothing except to illustrate the technique. The app presents a form with two rows: citruses and melons. A tap to the citrus or melon row displays below it a picker control with a list of their respective fruits for selection. Tapping the row again removes the picker and displays the selected fruit.
Using the Code
The app uses a UIFruitSelectionController
, a subclass of the UITableViewController
, to illustrate the technique. It’s responsible for displaying the fruit selections and the respective pickers.
To begin, define the variables and constants necessary to support the approach. The solution needs an array, rowMap
, to identify the position of each row and an array, rowsPerSection
, to identify the number of rows in each section of the table. This solution only has one section. There are two Boolean variables initialized to no, isMelonPickerDisplayed
, and isCitrusPickerDisplayed
, to identify when the respective pickers are displayed.
The constants define the number of sections, the section identifiers, their respective row identifiers and row counts. Two constants are required to identify the number of rows in the fruit section. The first row count, kFruitFormFruitSectionExpandedCount
, gives the count of the number of section rows when all the picker controls are visible, and similarly, the kFruitFormFrutSectionCollapsedCount
, gives the count when all the picker controls are hidden.
The remaining constants identify the section’s rows. One important aspect to note is that the constants naming the rows must order sequentially and correlate to the row positions they would have when all rows are displayed.
enum {
kFruitFormSectionCount = 1,
kFruitFormSectionFruits = 0,
};
enum {
kFruitFormFruitSectionExpandedCount = 4,
kFruitFormFruitSectionCollapsedCount = 2,
kFruitFormFruitSectionRowCitrus = 0,
kFruitFormFruitSectionRowCitrusPicker = 1,
kFruitFormFruitSectionRowMelon = 2,
kFruitFormFruitSectionRowMelonPicker = 3,
};
@interface UIFruitSelectionController ()
{
@private
NSInteger rowMap[kFruitFormFruitSectionExpandedCount];
NSInteger rowsPerSection[kFruitFormSectionCount];
Boolean isMelonPickerDisplayed;
Boolean isCitrusPickerDisplayed;
}
@end
Next, initialize the UIFruitSelectionController
’s, object variables in the viewDidLoad
, method.
Initially, the table positions of the picker rows are equal to the respective rows that present and remove them when tapped. This condition could be used to identify when the pickers are hidden. I prefer the clarity of flags rather than testing for equal mapping table row locations. Flags make the code more readable.
isMelonPickerDisplayed = isCitrusPickerDisplayed = false;
rowMap[kFruitFormFruitSectionRowCitrus] = kFruitFormFruitSectionRowCitrus;
rowMap[kFruitFormFruitSectionRowCitrusPicker] = kFruitFormFruitSectionRowCitrus;
rowMap[kFruitFormFruitSectionRowMelon] = kFruitFormFruitSectionRowCitrus+1;
rowMap[kFruitFormFruitSectionRowMelonPicker] = kFruitFormFruitSectionRowCitrus+1;
rowsPerSection[kFruitFormSectionFruits] = kFruitFormFruitSectionCollapsedCount;
self.tableView.estimatedRowHeight = 2;
Here are the methods for returning the number of sections and rows in each section. They use the constants and variables setup earlier to return the proper values.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return kFruitFormSectionCount;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return rowsPerSection[section];
}
The cellForRowAtIndexPath
, method tests for the cell to supply and returns it. The conditions to identify what cell to return have no difference between the static and dynamic ones. For this to work correctly, it’s necessary for the picker check to immediately follow the row that presents it; otherwise, if reversed, the picker will display instead.
Just before returning from the function, it tests if the returned row is one of the picker rows and calls its init
, method. This is necessary because the pickers are subclasses, and the UIPickerViewDataSource
, and UIPickerViewDelegate
, interfaces are defined in it. The call is required to initialize the UIPickerView
's, datasource and delegate properties. This is not illustrated in this article, however.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *cellIdentifier;
NSInteger row = [indexPath row];
NSInteger section = [indexPath section];
switch (section) {
case kFruitFormSectionFruits:
if (rowMap[kFruitFormFruitSectionRowCitrus] == row) {
cellIdentifier = @"CitrusCellId";
} else if (rowMap[kFruitFormFruitSectionRowCitrusPicker] == row) {
cellIdentifier = @"CitrusPickerCellId";
} else if (rowMap[kFruitFormFruitSectionRowMelon] == row) {
cellIdentifier = @"MelonCellId";
} else if (rowMap[kFruitFormFruitSectionRowMelonPicker] == row) {
cellIdentifier = @"MelonPickerCellId";
}
break;
default:
break;
}
UIFruitCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier
forIndexPath:indexPath];
if ((row == rowMap[kFruitFormFruitSectionRowCitrusPicker] && isCitrusPickerDisplayed)) {
[cell.pickerCitrus init];
} else if ((row == rowMap[kFruitFormFruitSectionRowMelonPicker] && isMelonPickerDisplayed)) {
[cell.pickerMelon init];
}
return cell;
}
The pickers are inserted and removed in the didSelectRowAtIndexPath
, method. Before the picker row is removed, the selection is copied from the picker and assigned to the display row. Next, the rowMap
, positions are decremented beginning at the picker row and for every row following it. The two helper routines, incrementRowMapAt
, and decrementRowMapAt
, are illustrated later in this article.
if (rowMap[kFruitFormFruitSectionRowCitrus] == row) {
if (isCitrusPickerDisplayed) {
isCitrusPickerDisplayed = NO;
NSIndexPath * removePath = [NSIndexPath indexPathForRow:rowMap
[kFruitFormFruitSectionRowCitrusPicker] inSection:kFruitFormSectionFruits];
UIFruitCell *citrusSelection = (UIFruitCell*)[tableView cellForRowAtIndexPath:removePath];
cell.lblCitrus.text = citrusSelection.pickerCitrus.selection;
--rowsPerSection[kFruitFormSectionFruits];
[self decrementRowMapAt:kFruitFormFruitSectionRowCitrusPicker];
[tableView deleteRowsAtIndexPaths:@[removePath] withRowAnimation:UITableViewRowAnimationMiddle];
}
}
The logic for adding the picker in the didSelectRowAtIndexPath
, is similar. The important aspect of this method is to increment the row position beginning at the picker and for every row that follows it.
isCitrusPickerDisplayed = YES;
++rowsPerSection[kFruitFormSectionFruits];
[self incrementRowMapAt:kFruitFormFruitSectionRowCitrusPicker];
NSIndexPath * insertPath = [NSIndexPath indexPathForRow:rowMap[kFruitFormFruitSectionRowCitrusPicker]
inSection:kFruitFormSectionFruits];
[tableView insertRowsAtIndexPaths:@[insertPath] withRowAnimation:UITableViewRowAnimationMiddle];
There are 2 helper methods that increment and decrement the rowMap
. They loop through the mapping table beginning at the passed in row position until it reaches the end and performs the respective operation.
- (void) incrementRowMapAt: (NSInteger) index {
for (NSInteger i = index; i < kFruitFormFruitSectionExpandedCount; i++) {
++rowMap[i];
}
}
- (void) decrementRowMapAt: (NSInteger) index {
for (NSInteger i = index; i < kFruitFormFruitSectionExpandedCount; i++) {
--rowMap[i];
}
}
This completes the description for using a mapping array to position dynamic rows in a form. Download the compressed project files to evaluate the approach.
History
- September 30, 2015 - Removed the link to github since the project files were added for download
- September 25, 2015 - Added example project files for download
- June 23, 2015 - Fixed some punctuation errors
- June 20, 2015 - Initial release