UICollectionView and UITableView are workhorse classes on iOS. As UICollectionView and 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.

Common Approaches

In UICollectionView Apple provides reloadSections: and reloadItemsAtIndexPaths: to update contents more granularly. Likewise, the UITableView class provides reloadRowsAtIndexPaths:withRowAnimation: and reloadSections:withRowAnimation:. Using reloadSections: or 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.

Throttled Reloading

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 UICollectionView or 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 UICollectionView and 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 UICollectionView and 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.

NSObject+ENHThrottledReloading Category

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 UICollectionView or 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 enh_minimumNanosecondsBetweenThrottledReloads property.

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.

Implementation Details

Mach Time

The core of the throttled reload category is in the enh_throttledReloadData method. Here is that method’s implementation.

-(void)enh_throttledReloadData
{
    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];

    ...

Here 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 minimumTimeDiffNanosecondsForUpdate and awaitingReload are retrieved in preparation for the following code.

...
    if(nanos > minimumTimeDiffNanosecondsForUpdate || lastReloadMachTime == 0.0)
    {
        [self setEnh_lastReloadMachTime:now];
        [self setEnh_awaitingReload:NO];
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:_cmd object:nil];
        if ([self respondsToSelector:@selector(reloadData)])
        {
            [self performSelector:@selector(reloadData)];
        }
        else
        {
            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.

Next the 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 enh_throttledReloadData.

Notice the category extends NSObject rather than UICollectionView or 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];
        [self setEnh_awaitingReload:YES];
    }
}

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 YES.

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.

Associated Objects

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.

#import <objc/runtime.h>

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 enh_lastReloadMachTime property.

    -(uint64_t)enh_lastReloadMachTime
    {
        NSNumber *value = objc_getAssociatedObject(self, (__bridge const void *)kENHLastReloadMachTimeAssociatedObjectKey);
        uint64_t lastReloadMachTime = [value unsignedLongLongValue];

        return lastReloadMachTime;
    }

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 uint64_t value.

Here is the setter.

-(void)setEnh_lastReloadMachTime:(uint64_t)enh_lastReloadMachTime
{
    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 uint64_t value.

Wrap Up

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 UICollectionView or UITableView instances in your iOS applications.

References

  1. UICollectionView Class Reference
  2. UITableView Class Reference
  3. NSObject+ENHThrottledReloading Category
  4. A Timing Utility
  5. Objective-C Associated Objects
  6. Apple’s Technical Q&A QA1398