Click here to Skip to main content
15,916,371 members
Articles / Mobile Apps / iPhone

How to Scroll a UITableView when Presenting a Form

Rate me:
Please Sign up or sign in to vote.
4.55/5 (14 votes)
15 Dec 2015CPOL6 min read 22.5K   171   8   1
This article details the necessary UITableViewDelegate methods required to fully support a form presented in a UITableView.

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:

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

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

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

Objective-C
- (IBAction)Save:(id)sender {


    UITableViewCell *cell; // The current cell

    NSString *name;        // Set to the current value of the UITextField in the current cell


    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.   

 

License

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


Written By
Program Manager
United States United States
I'm a senior technical leader with over 20 years of experience leading full-cycle software engineering and mobile application development projects for emerging and established brands. I've contributed to the success of multi-billion-dollar products and software solutions for companies like Lucent Technologies, Reuters, and Wolters Kluwer, and implemented enterprise-wide technology standards responsible for driving both cost savings and revenue growth. A frequent contributor to open source software, I also consult with technology startups to develop mobile applications and have published several articles on iOS development and software management processes.

Comments and Discussions

 
QuestionBrilliant Pin
BharathReddy V23-Jun-16 23:18
BharathReddy V23-Jun-16 23:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.