Introduction
The Xcode documentation is conspicuously thin about describing the UIKit interactions when scrolling a table view. The Table View Programming Guide for iOS leads a reader to believe all that's required to populate a table is to implement viewDidLoad
and tableView:cellForRowAtIndexPath
methods. The one mention about scrolling says, "Scrolling a table view also causes an invocation of the tableView:cellForRowAtIndexPath:
for each newly visible row." There are sections on populating a table with data, managing selections, and inserting and deleting rows to name a few, but there’s nothing dedicated to support scrolling.
This article describes the necessary methods in the UITableViewDelegate
protocol required to present and interact with a form to support scrolling. These methods also provide alternative, and possibly more elegant, ways to initialize cells with data as they are displayed.
Background
Presenting a form with the table view to capture user input can be deceptively simple: Drag a UITableViewController
widget onto a storyboard. Set the table view content to static cells. Configure the number of sections and cells. Drag some controls to each cell as appropriate for capturing the corresponding application data. Place a save button on the screen, perhaps in the navigation bar. Connect the button to a subclass of the UITableViewController
with a save action method. Add code in the save method to read the data in each cell, transfer it to the appropriate application variables, and store the data to the model.
It’s seemingly easy. Most, if not all, of the logic is in the save method. It is -- if the number of cells fits within the device's screen height. If not, only the data represented by the visible cells are manifest in the table view. As the user scrolls cells into view and others out, the cells scrolled off screen are removed and the cells becoming visible are added.
You may be asking why does this complicate things? It’s a form, so the application only needs the data when the user taps a button, such as save, to take action on the input. Here’s what happens when you only transfer the data from the view in the save method. As the save method accesses the cells from the top of the table to the bottom, tableView:cellForRowAtIndexPath:
returns nil
for the ones that are not in view. There’s no way to get at the invisible cells, and consequently, no way to transfer all the data that the user entered to the model.
Also, for tables with Dynamic Prototypes as new cells are scrolled into view, they are displayed with the cells that were taken off the screen. For example, if you have a table with 20 identical cells with each having one UITextField
and the first cell has a text field with the character '1', the cell scrolled in from the bottom has a text field with the character '1' if you do not initialize the cell in the tableView:cellForRowAtIndexPath:
method. The first cell is reused as is to display the cell scrolled into the view, but to initialize correctly, you must have stored the data when the cell scrolled off the screen.
Note, for tables with Static Cells, this isn’t a concern. The default behavior of the UITableView
and UITableViewController
maintains the values of the corresponding cells as they move in and out of view. While I’m unsure of the logic supporting this behavior, I do know the invisible cells are removed from the table as calls to tableView:cellForRowAtIndexPath:
returns nil
for them, so it is necessary to capture the data from these cells as they are scrolled out of view for access when the cell is outside the visible range.
Therefore, to correctly implement a form with a UITableView
two use cases must be handled. Cell data needs to be saved when it scrolls out of view (required for both Dynamic Prototypes and Static Cells) and it needs to be restored when it scrolls back (only required for tables with Dynamic Prototypes, but I would explicitly restore them anyway). When these two scenarios are properly handled, the save method can access and store all the entered data.
Using the code
Now that you have the background material for the problem, I will demonstrate a solution in an application that saves a list of 20 classmates. Each cell has one UITextField
control. In this example, save writes the value of each classmate to the system log. There are two subclasses of the UITableViewController
: one to demonstrate the logic for static cells called StaticClassListController
and the other called DynamicClassListController
to demonstrate the logic for Dynamic Prototypes.
To maintain the list of classmates as the cells scroll in and out of view, the DynamicClassListController
has a string array of 20 elements, called classmateNames
:
NSString *classmateNames[20];
The array is initialized to nil
for all 20 items in viewDidLoad
.
As rows are removed from view, the data is saved to the corresponding array index. This is handled in the tableView:didEndDisplayingCell:forRowAtIndexPath:
method in the DynamicClassListController
.
-(void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
UITextField* txtField = (UITextField*)[cell viewWithTag:1];
classmateNames[[indexPath row]] = txtField.text;
}
As rows become visible, the data is restored from the corresponding array index. This is handled in the tableView:willDisplayCell:forRowAtIndexPath:
method. This method is not required in table views with Static Cells.
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
UITextField *txtFieldClassMate = (UITextField*)[cell viewWithTag:1];
NSUInteger row = [indexPath row];
txtFieldClassMate.text = classmateNames[row];
}
The save method logic works by reading the value of the corresponding row's UITextField
if it is accessible, and if not, it reads the value from the classmateNames
array at the corresponding row index. The array only holds the most recent data for the invisible cells. It’s necessary to take the data directly from the visible cells since the user may have changed the values since the last time the cell’s data was transferred to the array.
- (IBAction)Save:(id)sender {
UITableViewCell *cell;
NSString *name;
for (int i = 0; i < 20; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell != nil) {
name = ((UITextField*)[cell viewWithTag:1]).text;
} else if (classmateNames[i] != nil) {
name = classmateNames[i];
} else {
name = @"Empty";
}
NSLog(@"Value for cell at row %d: %@", i+1, name);
}
}
In short, the requirements are as follows:
- Detect when the cell scrolls out of view with
tableView:didEndDisplayingCell:forRowAtIndexPath:
and save the cell's data to temporary storage. - Detect when the cell scrolls back into view with
tableView:willDisplayCell:forRowAtIndexPath:
and restore the cell's data from temporary storage if present. - And finally give priority to data in the visible cells over the temporary storage when the user initiates an action on the form, such as save.
Points of Interest
There’s one additional gotcha to handle. The default value for the Scroll View is Do Not Dismiss. The consequence is didEndDisplayingCell
is not called when the cell is scrolled out of view and its UITextField
is the first responder. If the user taps save while the cell remains out of view, cellForRowAtIndexPath
returns nil
, and consequently, the data is inaccessible. To correct this condition, set the keyboard property to Dismiss On Drag.
It’s commonly presented in tutorials to initialize cells with data in the tableView:cellForRowAtIndexPath:
method, but another approach is to initialize them in tableView:willDisplayCell:forRowAtIndexPath:
and leave the cell creation logic in tableView:cellFoRowAtIndexPath:
. Of course, there are many ways and reasons to skin the cat in application development. Knowing alternatives allows us to create the most elegant solutions for our unique design requirements.
Summary
A form is just one of the many reasons an application would have interest in detecting when cells scroll in and out of view. It is essential to detect them for forms having more cells than fit in the screen height.