Hi there. This post was originally written back in late 2010, a long time ago. Since then the API's and methods mentioned herein have changed a lot. I wont remove this post as it still may be helpful to some but be aware that it is pretty old.
It's one of the most used UI pieces in the iOS UIKit and its user interaction experience has set new benchmarks for scrolling feedback and touch interaction. The UITableView is a very simple UI piece but very complex under the hood. But this UI object requires a perfected implementation for it to perform fast, move smoothly, scale infinitely and have that particular bounce we all love.
The nuts and bolts
To understand how we can get the most out of this resource we have to understand how it works.
The UITableView is a child of the UIScrollView class where it inherits its scrolling and bouncy nature. Unlike most tables, the UITableView does not have definable columns. It operates primarily as a single column entity, you can create a custom UI in the cells to accommodate faux columns.
The table can take different forms for different data collections: a plain table view, grouped table view, a selection list or a section index. Accommodating for most collection types with easy access to customise any of the UI features.
It contains rows as UITableViewCell objects which can be custom or inherit a style from defaults available in the SDK. Cell's can operate with many different actions: Displaying a data collection, operating as a menu, giving a group of options to select or something completely custom. It's entirely up to the developer to choose how this object will work and will look.
Delegate and Data Source
Like most UIKit objects, the UITableView has a delegate class which allows the developer to modify functionality or the look of a UITableView. There is the UITableViewDataSource class which contains methods to feed information to the table's cells when requested.
The UITableViewDataSource class would be implemented as lightly and memory light as possible. Most of the methods in the data source are highly iterative meaning the can be called in quick succession at any time. If it is not implemented properly you can end up with performance and scrolling issues, which are explained in the next section.
The UITableView will call on its data source to request cells to display for the current viewport. UITableView is very smart when it comes to cutting down on memory usage. It will only request cells that are just about to appear in the viewport and deallocate any that have just exited the viewport.
As you can imagine, which is outlined in the next section, in a fast moving scroll you could have almost hundreds of calls to the tableView:cellForRowAtIndexPath: method. The scrolling has to wait for the method to return before it can move on. A call to that method that isn't almost instant will slow down feedback.
Now that the brief overview of the UI object is explained its now time to focus on the #1 issue that arises with UITableView objects.
Sluggish and jittery scrolling
A sluggish UITableView can be the death wish of any application. Personally, with an app that has a laggy or slow UITableView it immediately kills the experience. It is an expectation that has been set by most iOS applications that the view should be responsive down to the pixel on touch events. Many users will comment and rate an app based on how responsive it is and UITableView s are a common spot where it can slow down.
Whats going wrong?
How the UITableViewCell s are added to the interface is via the tableView:cellForRowAtIndexPath: method which is called on the defined UITableViewDataSouce. This method requires a lot of attention to memory management. The UITableView will call this method every time it needs to display a cell and only for cell's visible to the user. So when a UITableView is allocated and added to the interface it will run though its delegate and data source methods, finding out how many sections and rows there are, and then asking for cells for the visible rows. Extrapolating this, the tableView:cellForRowAtIndexPath: method could be called 8-10 times in the initial load up of the UITableView. Scrolling can drastically increase the load on this method. If the data set is large and the user can continuously scroll, the amount of calls to this method can be extremely large and successive. Could be in the region of 30-50 calls a second.
If the method does not properly manage memory and keep low on processing then this is where it can hold up the interface feedback. Using some simple ideas and methodologies the UITableView's in your next application can be as speedy as they can be.
I have previously covered this concept in a previous article. Lazy Loading is a practice where you put the allocation or processing of a variable or object down to the absolute last line before it's needed. If you don't need a UITextView allocated until line 25 then put it on line 24. Inversely you also release the variable or object as soon as possible so its not taking up and lingering in memory. It's these disciplines that can improve the speed of any part of an application and especially in highly iterative methods, like tableView:cellForRowAtIndexPath:.
Cut down on the UI
An interface design can be the cause of slowdowns in rendering a cell. Keeping the cell as visually clean and free as possible is recommended. Not only will it keep the speed up on scrolling but also give a clean, easy to use and appealing UI. Achieving this is easy: Keep it simple, use available UIKit styles and controls and keep the code footprint as light keeping any formatting and processing as low as possible.
Caching calculated or formatted results
This method is dependant on how you are feeding data to the Data Source class. Lets say you are using Core Data and you want to display the number of related records from another entity. The data displayed is not of a record but a menu, the example below. Lets look at a particular row in a UITableView I have been working on.
In the capture adjacent, the first 2 rows have detail text containing the of the number of sales for that time period. The fetch for the rows is quite complicated requiring: a NSCalendar instance for determining the range, the NSFetchRequest with the necessary NSPredicate's and fetch methods. So the method is quite intense for a simple output and with sequential iterations it could slow down user feedback.
In this instance I just created 2 new properties on my UITableViewController, todaysSales and monthsSales as NSNumber objects. Then as the row requested the particular information it would check to see if the corresponding property was nil and if it was it would fetch the value and store it in that property. The next time the same row is requested the value will be in the property todaysSales, as an example, and would bypass the Core Data fetch. However, this only works in a situation where it is acceptable, in my case there are only 2 properties I want to cache and uses little memory.
The only reason this works is the interface is a menu and does not directly correlate to a row in the store. They are summaries of data in the store so fetching and storing the simple result in the controller is acceptable.
Other ways of caching the data that requires formatting or processing are:
- Core Data: Store the property in the NSManagedObject, just add a property to the model's class and store and retrieve when needed.
- NSMutableArray & NSDictionary: Store your processed values in a NSDictionary and then in a global array property with the index as the row's index. This is not a nice way to do this and you will have to be very careful about memory management.
Using default UIKit styles
More noticeable with UITableViewCell's, there are different default styles that the SDK provides to build objects from. With UITableViewCell's there are 4 styles:
Using a style that best fits your design or designing around a style can save a lot of time in development and also can make your interface speedier. If you do need to go down the custom route there are 2 ways you can go about that. Either a XIB or a custom UIView class. There have not been any definitive tests to show XIB is better over a coded UIView class or vise-versa. You may like to code a UIView in code giving you more control over object allocation and that all important memory management, its up to you.
If you are displaying data from a Core Data store, a NSFetchedResultsController can help you with most of the leg work getting the UITableView segmented and displaying quickly. Segmenting a UITableView into sections from data in your store can be tricky but the NSFetchedResultsController class makes it painless, using the sectionNameKeyPath attribute.
What the NSFetchedResultsController does for you:
- Caches results application wide, identified by a cache key
- Manages the stored results for the controller
- Notifies your controller when records are updated or changed to allow the interface to reflect changes
- Segments data by a key into sections for the UITableView, see sectionNameKeyPath
- Simple methods to fill in the necessary UITableViewDataSource methods like tableView:numberOfRowsInSection:
If you have been using a NSArray or NSDictionary to pass around Core Data results you will find the NSFetchedResultsController a final proper solution to do what you need. It's not hard to implement and with little change to any existing code. It's just a matter of adding NSFetchedResultsControllerDelegate to your UITableViewController class, adding in the alloc methods for the request object on your controller and finally changing the data source methods to fetch their information from the NSFetchedResultsController property.
Atebits fast scrolling: Using CoreGraphics
Developer of the much loved Twitter for iPhone, formerly Tweetie, application has revealed an insight into a method to speeding up a UITableViewCell even more. It uses a technique where you render the cell's content using CoreGrahpics on a custom UITableViewCell impelementation.
Basically the technique is to give the table a cell that has been flattened. Traditionally you would layer UITextView's and UIImageView's on the UITableViewCell and that would be that. This method makes you do the same but render the layers out to a flattened layer. It's not a technique that you may have to implement but its worth knowing the deeper operations of the UITableView.
So when the cell is requested, the custom UITableViewCell class is allocated and initialised. Then the drawContentView: method is called and what happens next is the layers are then drawn onto the UIView, like flattening or rasterising a Photoshop document from many layers to one layer. This way the cell is a rendered graphic that the UITableView can throw around with ease.
For more information and sample code on this practice you can see Atebits post: http://blog.atebits.com/2008/12/fast-scrolling-in-tweetie-with-UITableView/
This is not common in all applications however with a graphically intense cell this may be a better option than a typical layered UIView.
Getting down to brass tacks
In a summary of what has been covered there are a few things to always have in your mind when developing a UITableView:
- Keep the data source methods as light as possible.
- Manage your memory efficiently and implement the Lazy Loading practice.
- Investigate alternative User Interfaces to cut down or refine the cell to make it faster to display.
- Use helpful classes in the API, like NSFetchedResultsController, where applicable
- Analyse your code using Instruments and make sure there are no leaks.