UITableView are workhorse classes on iOS. As
UITableView contents become more complex and dynamic in your iOS apps it can prove difficult to maintain UI state and performance, especially when dealing with dynamic layout in
UICollectionView or variable row height in
UITableView. Oftentimes, the simplest code path is to simply
reloadData. Unfortunately, calling
reloadData frequently can incur a significant performance penalty. This performance penalty is especially apparent when the underlying data updates frequently, such as rapid updates from a web service or continuous updates in response to user input, causing your app to stutter. In the following article we examine a few possible approaches to improving UI performance when dealing with rapid updates and document an approach that has worked well on our projects.
UICollectionView Apple provides
reloadItemsAtIndexPaths: to update contents more granularly. Likewise, the
UITableView class provides
reloadItemsAtIndexPaths: often requires significant state tracking code to maintain synchronization of content and layout with data model objects. Calling
reloadData to update the UI can prove a bit heavy-handed if only a small subset of the UI is actually changing, but it is often by far the simplest code path.
You might choose to observe attribute state by reloading only the impacted index paths or sections using the granular reloading methods Apple provides. This is the preferred approach since the cells may be dynamically sized. Existing code can be reused to update cell contents in a consistent manner. In many cases this is the proper approach, so long as data updates, and thus reloads, are sufficiently far apart temporally so the main thread is not saturated with UI and layout updates.
To solve performance issues, you might choose to observe attribute state with cells directly. This can prove fragile and tricky to implement and it might not present a good fit, especially if cell contents impact layout (i.e. dynamic cell size). Implementations that use this approach also break encapsulation, having the cells observe data model objects for changes directly instead of within a more appropriate controller object.
What about situations where attribute state can change rapidly, or there is a large volume of changes to process in a short period of time? The granular reloading approach mentioned above can certainly help target updates to only those areas that change. However, you may notice performance degradation as the number of data updates increases and the impacted cells reload. When the number of cells in a
UITableView needs to change in response to changes in its data source we have often found that inserting and deleting cells can make view to model synchronization difficult. Small errors in state tracking can lead to a crash like the one below.
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'Invalid update: invalid number of items in section 0. The number of items
contained in an existing section after the update (35) must be equal to the number
of items contained in that section before the update (35), plus or minus the number
of items inserted or deleted from that section (0 inserted, 1 deleted) and plus or
minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
To address this issue we’ve created a category that extends
UITableView. This category limits the rate of calls to
reloadData by setting a minimum rate limiting, or “throttle” interval. Limiting the rate of calls to
reloadData allows coalescing multiple content updates into a single
reloadData call, thus allowing the app to balance update frequency versus responsiveness. We have found that rate limiting
UITableView reloads reduces code complexity by eliminating state tracking code for determining which index paths or sections require updating on the UI. This category allows you to maintain your straightforward view controller implementation as the frequency of updates and data you display becomes more complex in your apps.
Get the Code
The NSObject+ENHThrottledReloading category is available on github under the MIT license.
Using Throttled Reloading
Using the throttled reloading category is straightforward. Import the category’s header file and set the
enh_minimumNanosecondsBetweenThrottledReloads property on your instance of
UITableView. Then call
enh_throttledReloadData instead of
reloadData as your UI needs to be updated. If a call to
enh_throttledReloadData hasn’t occurred within the minimum time interval the reload will occur immediately. Otherwise the reload will be delayed until the time interval expires. Calling
enh_throttledReloadData multiple times in rapid succession will result in no more than one call per time interval you set using the
Note: Any calls made directly to
reloadData will be executed normally. Further, any direct calls to
reloadData will not impact the throttled reloading. Calls to
enh_throttledReloadData will not be canceled by calling ‘reloadData` directly.
Experiment with the value used for
enh_minimumNanosecondsBetweenThrottledReloads to determine the interval that works best for your use case. This will allow you to find the right balance between update frequency and responsiveness. The interval can be set to a fairly short time so the user is unlikely to notice lag. We have found that 0.3 seconds (or 18 frames at 60 fps) is a good starting point for a number of our use cases, and the category uses this as the default value. We have found that this short delay frees the processor sufficiently without introducing a noticeable delay.
When your object is no longer in use, call
enh_cancelPendingReload to cancel any pending reloads. Typically this is called inside the
dealloc implementation of the object that initiates throttled calls. Doing so will prevent exceptions from being raised, otherwise it’s possible that a deferred reload could be called on an object after being deallocated.
The core of the throttled reload category is in the
enh_throttledReloadData method. Here is that method’s implementation.
uint64_t now = mach_absolute_time ();
uint64_t lastReloadMachTime = [self enh_lastReloadMachTime];
uint64_t timeSinceLastUpdate = now - lastReloadMachTime;
mach_timebase_info_data_t timebaseInfo = [self enh_timebaseInfo];
uint64_t nanos = timeSinceLastUpdate * timebaseInfo.numer / timebaseInfo.denom;
uint64_t minimumTimeDiffNanosecondsForUpdate = [self enh_minimumNanosecondsBetweenThrottledReloads];
BOOL awaitingReload = [self enh_awaitingReload];
mach_absolute_time is used to determine the current time. A time interval is then calculated using current and previous time stamps and stored in
timeSinceLastUpdate. It is compared to the previous time stamp. The mach time value is converted into nanoseconds and the
awaitingReload are retrieved in preparation for the following code.
if(nanos > minimumTimeDiffNanosecondsForUpdate || lastReloadMachTime == 0.0)
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:_cmd object:nil];
if ([self respondsToSelector:@selector(reloadData)])
NSAssert(NO, @"object does not respond to reloadData selector");
In the above code we check to see if the number of nanoseconds that have elapsed is greater than the minimum time interval. We also check to see if the time stamp of the last reload is zero, which indicates that this is the first time through this code. If either of the above is true,
reloadData is called.
enh_lastReloadMachTime is set to the the current time stamp (
now) so we can calculate the time interval the next time
enh_throttledReloadData is called. We also set the
enh_awaitingReload flag to
NO and cancel any previous perform requests to
Notice the category extends
NSObject rather than
UITableView directly. This allows the code to be applied to either since they both contain a
reloadData method. The category can also be used to extend any class that conforms to the
ENHThrottledReloading protocol by implementing a
reloadData method. Since the category can be used to extend any
NSObject subclass, we perform a check to ensure
reloadData is implemented before attempting to call it.
else if (!awaitingReload)
NSTimeInterval delay = ((double)minimumTimeDiffNanosecondsForUpdate - nanos) / NSEC_PER_SEC;
[self performSelector:_cmd withObject:nil afterDelay:delay];
Finally if the class is not already awaiting a reload, one is scheduled to run after a delay and the
enh_awaitingReload flag is set to
Special thanks to Mark Dalrymple at The Big Nerd Ranch for sharing his “A Timing Utility” post that inspired this category. Mark’s post explains how to use mach time for precise timing on Mac OS X and iOS for performance tuning. Keeping accurate track of time without the overhead of
NSDate is crucial to the throttled reloading category.
Introduced in Objective-C 2.0, Objective-C associated objects allow objects to be associated at runtime in lieu of storing those associations in instance variables. Our category needs to track some internal state to perform throttled reloading, so we’ve added a few properties in the category. Categories in Objective-C cannot directly add instance variables to an object, which are normally used to store values for properties. Associated objects (also know as associative references) are used to store the values needed by the properties added in the category.
For example, the code above illustrates how associated objects are used in the category to track the last reload mach time state.
static NSString *kENHLastReloadMachTimeAssociatedObjectKey = @"com.enharmonichq.lastReloadMachTime";
First we import the Objective-C runtime, which contains the C functions for working with associated objects. We also create a constant to use for getting or setting the associated object. This constant acts as a key to associate one object with another in the Objective-C runtime.
Here is the getter for the
NSNumber *value = objc_getAssociatedObject(self, (__bridge const void *)kENHLastReloadMachTimeAssociatedObjectKey);
uint64_t lastReloadMachTime = [value unsignedLongLongValue];
The Objective-C runtime allows associating objects to each other, not raw values, so we need to wrap any scalar values in an Objective-C object. The getter retrieves the
NSNumber value for the
kENHLastReloadMachTimeAssociatedObjectKey from the Objective-C runtime which is then unwrapped to return the raw
Here is the setter.
objc_setAssociatedObject(self, (__bridge const void *)kENHLastReloadMachTimeAssociatedObjectKey, @(enh_lastReloadMachTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
Similar to the getter, the setter creates a keyed relationship between the instance and an
NSNumber wrapping a raw
With technologies like the Objective-C Associated Objects and Mach Absolute Time Units, iOS and OS X have many tools to round out your repertoire. As shown above these tools can be invaluable when optimizing performance and simplifying code. We hope you find this category helpful for addressing performance issues when dealing with rapid updates to
UITableView instances in your iOS applications.