Click here to Skip to main content
12,830,643 members (44,142 online)
Click here to Skip to main content
Add your own
alternative version

Stats

11.5K views
151 downloads
8 bookmarked
Posted 23 Sep 2015

How to Scroll a UITableView when Presenting a Form

, 15 Dec 2015 CPOL
Rate this:
Please Sign up or sign in to vote.
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:

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; // 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)

Share

About the Author

Will J Miller
Program Manager
United States United States
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionBrilliant Pin
BharathReddy V24-Jun-16 0:18
memberBharathReddy V24-Jun-16 0: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.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.170326.1 | Last Updated 15 Dec 2015
Article Copyright 2015 by Will J Miller
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid