Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / iOS

How to Design Dynamically Appearing Rows in A UITableView

4.13/5 (6 votes)
22 Jun 2015CPOL4 min read 23.2K   47  
Describes a design approach for implementing dynamic rows in a UITableView

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.

Objective-C
enum {

    kFruitFormSectionCount = 1,  // only one section in this solution

    kFruitFormSectionFruits = 0, //  the fruit section id

};

enum {  

    kFruitFormFruitSectionExpandedCount = 4,

    kFruitFormFruitSectionCollapsedCount = 2,

    kFruitFormFruitSectionRowCitrus = 0,

    kFruitFormFruitSectionRowCitrusPicker = 1,

    kFruitFormFruitSectionRowMelon = 2,

    kFruitFormFruitSectionRowMelonPicker = 3,

};

@interface UIFruitSelectionController ()
{

@private    

    NSInteger rowMap[kFruitFormFruitSectionExpandedCount]; // maps row type to table position

    NSInteger rowsPerSection[kFruitFormSectionCount];  // total count of rows in per section 

    Boolean   isMelonPickerDisplayed;  // Yes if the melon picker is displayed

    Boolean   isCitrusPickerDisplayed; // No if the Citrus picker is displayed.

}
@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.

C#
// Pickers are initially not displayed

isMelonPickerDisplayed = isCitrusPickerDisplayed = false;

// Initialize the location of each row type in the table

rowMap[kFruitFormFruitSectionRowCitrus] = kFruitFormFruitSectionRowCitrus;

rowMap[kFruitFormFruitSectionRowCitrusPicker] = kFruitFormFruitSectionRowCitrus;

rowMap[kFruitFormFruitSectionRowMelon] = kFruitFormFruitSectionRowCitrus+1;

rowMap[kFruitFormFruitSectionRowMelonPicker] = kFruitFormFruitSectionRowCitrus+1;

rowsPerSection[kFruitFormSectionFruits] = kFruitFormFruitSectionCollapsedCount;

self.tableView.estimatedRowHeight = 2; // work around for bug in tableView

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.

Objective-C
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

    // Return the number of sections.
    return kFruitFormSectionCount;

}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    // Return the number of rows in the 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.

Objective-C
- (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.

Objective-C
if (rowMap[kFruitFormFruitSectionRowCitrus] == row) { // Citrus selection row tapped?

  if (isCitrusPickerDisplayed) {  //  Citrus picker is displayed so
                   
   isCitrusPickerDisplayed = NO; // after this routine exits the citrus picker is no longer displayed

   // create the index for the citrus picker                
   NSIndexPath * removePath = [NSIndexPath indexPathForRow:rowMap
		[kFruitFormFruitSectionRowCitrusPicker] inSection:kFruitFormSectionFruits];


   // Retrieve a copy of the cell               
   UIFruitCell *citrusSelection = (UIFruitCell*)[tableView cellForRowAtIndexPath:removePath];

   // Copy the selection to the row that displays it.  
   cell.lblCitrus.text = citrusSelection.pickerCitrus.selection;

   --rowsPerSection[kFruitFormSectionFruits]; // one less row after the picker is removed.

   // all the rows from the picker and after shift up in the table by one position
   [self decrementRowMapAt:kFruitFormFruitSectionRowCitrusPicker];                   

   // Remove the picker from the table.
   [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.

Objective-C
isCitrusPickerDisplayed = YES;  // Set indicating the citrus picker is on display

++rowsPerSection[kFruitFormSectionFruits];  // once inserted there is one more row in the table

// all the rows from the picker and after shift down in the table by one position 
// to make room for the cell
[self incrementRowMapAt:kFruitFormFruitSectionRowCitrusPicker];

// Create the index path for the picker
NSIndexPath * insertPath = [NSIndexPath indexPathForRow:rowMap[kFruitFormFruitSectionRowCitrusPicker] 
				inSection:kFruitFormSectionFruits];

// Insert the picker after the row that when tapped presents it

[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.

Objective-C
- (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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)