iOS UITableViews: Simplifying Code for Dynamic Static Content

This is a reposting with minor edits of an article I wrote for Glassdoor. Time has moved on, and Objective-C is out of vogue for code examples on iOS, but the concept outlined here is not dependent on the language used, so I haven't taken the time to convert the examples into Swift. For now, that is, as they say, left to the reader as an exercise...

Sometimes very simple requirements are answered with straight forward and direct software design that can, over time, become complex and difficult to maintain. This is often seen in app ‘Settings’, ‘About’, ‘hamburger’ and similar screens. This post shows a technique to simplify the code for these screens.

I’ll start by showing how the code for a hypothetical Settings screen can spiral out of control. Then I’ll share a simple structure for refactoring the code to make it easily scale as the screen gains more and more options.

Our hypothetical Settings screen may contain a simple listing of options that a user can choose to interact with or navigate into. At first, it doesn’t even contain any settings, but we’ll get there. Our initial requirements are quite simple, as shown in Figure 1 below:

Figure 1: Mockup of the first iteration of a settings screen.

Figure 1: Mockup of the first iteration of a settings screen.

Touching either the ‘Terms and Conditions’ or the ‘Privacy Policy’ rows will navigate the user to a new screen to view the respective information.

In iOS, this UI is commonly implemented using a UITableView. This allows for easy addition of more options on the Settings screen over time by adding rows to the table.

UITableViews are great for displaying a variable number of rows of dynamic data. However, in this case, the table is asked to display a small and known number of rows which contain static data. It’s expected that the data displayed remains constant across viewings of the table. I refer to tables like this one as a “static table”. I refer to a typical table showing a variable number of rows as a “dynamic table”.

In a dynamic table, the data is typically supplied via an array of model data. The table view’s data source uses this array of data to supply the table with everything it needs. The number of rows in the table equals the number of records in the array and each row is populated with the data from the corresponding data record in the array.

In a static table view, there is typically no ready array of data for the data source to draw upon. The content of the table view is instead defined directly in the functional requirements.

 

The Basic Implementation

The problem seems simple enough. Our settings screen has only two rows that allow the user to view Terms and Conditions or the Privacy Policy for the app. Since the number of rows is small and there is no ready array of data to draw upon, a common implementation hard codes the table information in the table’s data source and delegate methods. The sample code below shows the first iteration of three methods of interest from the UITableViewDataSource and UITableViewDelegate protocols that I’ll update as this example progresses.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
   return 2;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   NSInteger row = indexPath.row;
   UITableViewCell *cell;
 
   // Assume the cellWithTitle method used below is defined elsewhere
   if (row == 0) {
      cell = [self cellWithTitle:@“Terms and Conditions”];
   } else if (row == 1) {
      cell = [self cellWithTitle:@“Privacy Policy”];
   }
 
   return cell;
}
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   NSInteger row = indexPath.row;
 
   // Assume the methods called below are defined elsewhere
   if (row == 0) {
      [self showTermsAndConditions];
   } else if (row == 1) {
      [self showPrivacyPolicy];
   }
}


Now with complications!

Over time, we’re likely to upgrade our list of options on the Settings screen. In this example, a new requirement calls for a new row in our static table view. It will allow the user to log out of a third party service if they had logged into it while using our app. It should be shown as the first row, but only if the user is currently logged into the third party service from our app. Otherwise, no row is shown. The Settings screen will have two possible configurations as shown in Figure 2 below:

Figure 2: Mockups of two possible Setting screen configurations after adding the possibility of a third party logout.

Figure 2: Mockups of two possible Setting screen configurations after adding the possibility of a third party logout.

Our static table is now a “dynamic static table”. The set of all possible content is static, but we may only show a subset of the content at one time. This means that depending on which rows are showing, the row numbers will correspond to different outcomes when the user touches the row. For example, in the figures above, row 0 of the left configuration will lead the user to the Terms and Conditions, but row 0 on the right configuration will log the user out of the Third Party service. Continuing with the previous methodology, the code might look like this:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
   NSInteger numRows = 2;
 
   if (self.currentUser.isThirdPartyAuthenticated) {
      numRows++;
   }
 
   return numRows;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   NSInteger row = indexPath.row;
   UITableViewCell *cell;
 
   if (self.currentUser.isThirdPartyAuthenticated) {
      if (row == 0) {
         cell = [self cellWithTitle:@“Third Party Log Out”];
      } else if (row == 1) {
         cell = [self cellWithTitle:@“Terms and Conditions”];
      } else if (row == 2) {
         cell = [self cellWithTitle:@“Privacy Policy”];
      }
   } else {
      if (row == 0) {
         cell = [self cellWithTitle:@“Terms and Conditions”];
      } else if (row == 1) {
         cell = [self cellWithTitle:@“Privacy Policy”];
      }
   }
 
   return cell;
}
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   NSInteger row = indexPath.row;
 
   if (self.currentUser.isThirdPartyAuthenticated) {
      if (row == 0) {
         [self logoutThirdParty];
      } else if (row == 1) {
         [self showTermsAndConditions];
      } else if (row == 2) {
         [self showPrivacyPolicy];
      }
   } else {
      if (row == 0) {
         [self showTermsAndConditions];
      } else if (row == 1) {
         [self showPrivacyPolicy];
      }
   }
}

There are already red flags in the code with obvious duplications occurring but it still seems manageable, so we continue on.

Now, more upgrades… we’re going to add a row for our own membership log in and log out as the first row in our settings table. We’re also going to add a debug only row as the last row that will allow testers to see more information about the settings of a running app. The Settings screen will have four possible configurations, as shown in Figure 3 below:

Figure 3: Mockups of four possible Settings screen configurations after adding native log in/out and debug settings.

Figure 3: Mockups of four possible Settings screen configurations after adding native log in/out and debug settings.

The updated code could look like this:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
   NSInteger numRows = 3;
 
   if (self.currentUser.isThirdPartyAuthenticated) {
      numRows++;
   }
 
#ifdef DEBUG
   numRows++;
#endif
 
   return numRows;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   NSInteger row = indexPath.row;
   UITableViewCell *cell;
 
   if (self.currentUser.isThirdPartyAuthenticated) {
      if (row == 0) {
         if (self.currentUser.isAuthenticated) {
            cell = [self cellWithTitle:@“Log Out”];
         } else {
            cell = [self cellWithTitle:@“Log In”];
         }
      } else if (row == 1) {
         cell = [self cellWithTitle:@“Third Party Log Out”];
      } else if (row == 2) {
         cell = [self cellWithTitle:@“Terms and Conditions”];
      } else if (row == 3) {
         cell = [self cellWithTitle:@“Privacy Policy”];
      }
   } else {
      if (row == 0) {
         if (self.currentUser.isAuthenticated) {
            cell = [self cellWithTitle:@“Log Out”];
         } else {
            cell = [self cellWithTitle:@“Log In”];
         }
      } else if (row == 1) {
         cell = [self cellWithTitle:@“Terms and Conditions”];
      } else if (row == 2) {
         cell = [self cellWithTitle:@“Privacy Policy”];
      }
   }
 
#ifdef DEBUG
   if (!cell) {
      cell = [self cellWithTitle:@“Debug Settings”];
   }
#endif
 
   return cell;
}
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   NSInteger row = indexPath.row;
 
   if (self.currentUser.isThirdPartyAuthenticated) {
      if (row == 0) {
         if (self.currentUser.isAuthenticated) {
            [self logOutUser];
         } else {
            [self showLogIn];
         }
      } else if (row == 1) {
         [self logoutThirdParty];
      } else if (row == 2) {
         [self showTermsAndConditions];
      } else if (row == 3) {
         [self showPrivacyPolicy];
      }
#ifdef DEBUG
      else {
         [self showDebugSettings];
      }
#endif
   } else {
      if (row == 0) {
         if (self.currentUser.isAuthenticated) {
            [self logOutUser];
         } else {
            [self showLogIn];
         }
      } else if (row == 1) {
         [self showTermsAndConditions];
      } else if (row == 2) {
         [self showPrivacyPolicy];
      }
#ifdef DEBUG
      else {
         [self showDebugSettings];
      }
#endif
   }
}

Clearly, this is insane. Every conditionally visible row will require a code branch to handle the change in which row corresponds to which action. We have two conditionally visible rows now, the third party log out and the debug settings row, but we took advantage of the fact that the debug settings row always occurs last to slightly reduce the code complexity.

 

The Issues

There are two issues that have made the code to handle a static table view even more complex than the code for a typical dynamic table.

  1. A row index does not have a fixed correlation to the action taken when the row is touched.

  2. Checks for current state to determine which rows are available are distributed throughout the code.

 

The Refactor

To address the first issue, we can define row types. Each type will correspond directly to the action that should be taken when a row of a given type is touched. This will simplify our tableView:didSelectRowAtIndexPath: method.

After our row types are defined, we’ll use them while addressing our second issue. At the top of the implementation file, we’ll add an enumeration for the row types.

// Tip: Start the row enumeration at a value other than zero. 
//	Since the default tag value on a cell is zero (like all UIViews), 
//	it’s easy to confuse a default assigned tag with a RowType enum value of zero, 
//	leading to subtle bugs.
typedef NS_ENUM(NSInteger, RowType) {
   RowTypeLogIn = 1,
   RowTypeLogOut,
   RowTypeThirdPartyLogOut,
   RowTypeTermsAndConditions,
   RowTypePrivacyPolicy,
   RowTypeDebugSettings
};

To address the second issue, we can mimic the way that dynamic table views are often implemented. A dynamic table usually draws data from a data structure like an array. Factors affecting the table’s rows have already been taken into consideration in constructing the data structure and don’t need to be addressed in the table’s data source methods.

In this example, we can construct our own data structure, an array of dictionaries, that the table view data source and delegate can reference. Each dictionary in the array describes one row that should be displayed in the table. All the state checks that affect the set of table rows are pulled into the construction of the data structure. The result is a data structure that only includes the rows that should be visible to the user right now.

We can create our row descriptions in another method of our implementation file. Note, this sample code doesn’t address reducing overhead from creation of the row descriptions… it’s just recreated every time it’s requested.

- (NSArray *)rowDescriptors {
   // Set up for our own log in/out row
   NSString *logInOutTitle = (self.currentUser.isAuthenticated ? @“Log Out” : @“Log In”);
   RowType logInOutType = (self.currentUser.isAuthenticated ? RowTypeLogOut : RowTypeLogIn);
 
   // Save conditionally visible rows to variables to ease later removal
   NSDictionary *thirdPartyRowDescriptor = @{@“title”: @“Third Party Log Out”,
                                             @“type”: @(RowTypeThirdPartyLogOut)};
 
   NSDictionary *debugSettingsRowDescriptor = @{@“title”: @“Debug Settings”,
                                                @“type”: @(RowTypeDebugSettings)}
 
   // Construct the complete data source first
   NSMutableArray *descriptors = 
                     [@[@{@“title”: logInOutTitle,
                          @“type”: logInOutType},
                        thirdPartyRowDescriptor,
                        @{@“title”: @“Terms and Conditions”,
                          @“type”: @(RowTypeTermsAndConditions)},
                        @{@“title”: @“Privacy Policy”,
                          @“type”: @(RowTypePrivacyPolicy)},
                        debugSettingsRowDescriptor] mutableCopy];
 
   // Do checks to determine which rows should be removed
   if (!self.currentUser.isThirdPartyAuthenticated) {
      [descriptors removeObject:thirdPartyRowDescriptor];
   }
 
#ifndef DEBUG
   [descriptors removeObject:debugSettingsRowDescriptor];
#endif
 
   return [NSArray arrayWithArray:descriptors];
}

Now, the three sample methods can be updated to use the new row descriptions and types.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
   return [[self rowDescriptors] count];
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   NSDictionary *rowDescriptor = [self rowDescriptors][indexPath.row];
 
   UITableViewCell *cell = [self cellWithTitle:rowDescriptor[@“title”];
	
   // Tip: Set the cell tag equal to the row type for easy identification of the
   //	   cell in delegate callbacks that provide the cell
   cell.tag = [rowDescriptor[@“type”] integerValue];
 
   return cell;
}
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   NSDictionary *rowDescriptor = [self rowDescriptors][indexPath.row];
   RowType rowType = [rowDescriptor[@“type”] integerValue];
 
   switch(rowType) {
      case RowTypeLogIn:              [self showLogIn];              break;
      case RowTypeLogOut:             [self logOutUser];             break;
      case RowTypeDropBoxLogOut:      [self logoutDropBox];          break;
      case RowTypeTermsAndConditions: [self showTermsAndConditions]; break;
      case RowTypePrivacyPolicy:      [self showPrivacyPolicy];	     break;
      case RowTypeDebugSettings:      [self showDebugSettings];	     break;
   }
}

It’s a big improvement for a simple refactor. The refactored approach has a number of benefits:

  1. The complexity of these methods has been greatly reduced.

  2. The factors affecting which rows will be visible are centralized in the creation of our local data structure via the rowDescriptors method.

  3. A row’s location in the table can change without breaking code since the code no longer relies on the row’s index.

  4. Overall readability and maintainability of the code is greatly improved.

This technique of directly associating a row type to an action could be extended for use in any static, index based UI. For example, consider a UICollectionView with static content that represents a series of currently applied filter tags with an interface like the one shown in Figure 4 below.

Figure 4: Mockup of a filter display that is implemented with a UICollectionView.

Figure 4: Mockup of a filter display that is implemented with a UICollectionView.

The data can be filtered in a fixed number of ways and only the ways that are currently applied are displayed. In that case, the index of a pill doesn’t directly correlate to its effect. Assigning a type to each item in the collection view and referencing the type instead of item indexes simplifies the code in the same manner as the static table view example above.