{"id":4259,"date":"2016-02-03T16:55:14","date_gmt":"2016-02-03T16:55:14","guid":{"rendered":"https:\/\/ushipblogsubd.wpengine.com\/?p=4259"},"modified":"2025-09-03T16:05:26","modified_gmt":"2025-09-03T16:05:26","slug":"populating-uitableviewcells-asynchronously-to-fix-uitableview-lag","status":"publish","type":"post","link":"https:\/\/ushipblogsubd.wpengine.com\/shipping-code\/populating-uitableviewcells-asynchronously-to-fix-uitableview-lag\/","title":{"rendered":"Populating UITableViewCells Asynchronously to Fix UITableView Lag"},"content":{"rendered":"<p><i>Note: This article was originally published on <a href=\"https:\/\/github.com\/pepaslabs\/GlitchyTable\" target=\"_blank\" rel=\"noopener\">Github<\/a><\/i><\/p>\n<h2>Abstract<\/h2>\n<p>In general, the key to keeping the iPhone UI responsive is to avoid blocking the main thread. In the case of <tt>UITableView<\/tt>, this boils down to keeping <tt>tableView(_:cellForRowAtIndexPath:)<\/tt> as performant as possible.<\/p>\n<p>However, sometimes it is simply not possible to marshall all of the data needed to populate a complex <tt>UITableViewCell<\/tt> without causing a drop in frame rate. In these cases, it is necessary to switch to an asynchronous strategy for populating <tt>UITableViewCells<\/tt>. \u00a0In this article we explore a trivial example of using this technique.<\/p>\n<h2>Demonstrating the Problem<\/h2>\n<p>Our first task is to create a simple Xcode project which demonstrates the problem of a slow model leading to laggy scrolling performance.<\/p>\n<p>We start with a model which blocks for 100ms. This simulates the lag induced by excessive disk access, complex <tt>CoreData<\/tt> interactions, etc:<\/p>\n<pre class=\"lang:swift theme:twilight\">class GlitchyModel\r\n{\r\n    func textForIndexPath(indexPath: NSIndexPath) -&gt; String\r\n    {\r\n        NSThread.sleepForTimeInterval(0.1)\r\n        return \"(indexPath.row)\"\r\n    }\r\n}\r\n<\/pre>\n<p>We then hook that model up with some typical boilerplate code in our <tt>GlitchyTableViewController<\/tt> implementation:<\/p>\n<pre class=\"lang:swift theme:twilight\">override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath)\r\n{\r\n    if let cell = cell as? GlitchyTableCell\r\n    {\r\n        _configureCell(cell, atIndexPath: indexPath)\r\n    }\r\n}\r\n\r\nprivate func _configureCell(cell: GlitchyTableCell, atIndexPath indexPath: NSIndexPath)\r\n{\r\n    cell.textLabel?.text = model.textForIndexPath(indexPath)\r\n}\r\n<\/pre>\n<p>(For additional context, see the <a href=\"https:\/\/github.com\/pepaslabs\/GlitchyTable\/tree\/master\/1%20The%20Problem\/GlitchyTable\" target=\"_blank\" rel=\"noopener\">Xcode project<\/a> and <a href=\"https:\/\/github.com\/pepaslabs\/GlitchyTable\/blob\/master\/1%20The%20Problem\/GlitchyTable\/GlitchyTable\/GlitchyTableViewController.swift\" target=\"_blank\" rel=\"noopener\">source code<\/a> of this solution).<\/p>\n<p>The result is a <tt>UITableView<\/tt> with terrible scrolling performance, as shown in this <a href=\"http:\/\/gfycat.com\/ImpassionedBoilingCoyote\" target=\"_blank\" rel=\"noopener\">video<\/a>:<\/p>\n<p>[evp_embed_video url=&#8221;https:\/\/github.com\/pepaslabs\/GlitchyTable\/raw\/master\/1%20The%20Problem\/1_laggy_table.mp4&#8243; loop=&#8221;true&#8221; autoplay=&#8221;true&#8221; width=&#8221;374&#8243; ratio=&#8221;1.777&#8243;]<\/p>\n<h2>Populating <tt>UITableViewCell<\/tt> Asynchronously<\/h2>\n<p>Our first attempt at solving this problem is to rewrite <tt>_configureCell(_,atIndexPath)<\/tt> such that it dispatches to a background thread to grab the data from the model, then jumps back to the main thread to populate the <tt>UITableViewCell<\/tt>:<\/p>\n<pre class=\"lang:swift theme:twilight\">private func _configureCell(cell: GlitchyTableCell, atIndexPath indexPath: NSIndexPath)\r\n{\r\n    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -&gt; Void in\r\n\r\n        let text = self.model.textForIndexPath(indexPath)\r\n\r\n        dispatch_async(dispatch_get_main_queue(), { () -&gt; Void in\r\n\r\n            cell.textLabel?.text = text\r\n\r\n        })\r\n    })\r\n}\r\n<\/pre>\n<p>(See also the <a href=\"https:\/\/github.com\/pepaslabs\/GlitchyTable\/tree\/master\/2%20Buggy%20Solution\/GlitchyTable\" target=\"_blank\" rel=\"noopener\">Xcode project<\/a> and <a href=\"https:\/\/github.com\/pepaslabs\/GlitchyTable\/blob\/master\/2%20Buggy%20Solution\/GlitchyTable\/GlitchyTable\/GlitchyTableViewController.swift\" target=\"_blank\" rel=\"noopener\">source code<\/a> of this solution).<\/p>\n<p>This change is enough to solve the laggy scrolling performance (as seen in this <a href=\"http:\/\/gfycat.com\/OnlyAmusingCardinal\" target=\"_blank\" rel=\"noopener\">video<\/a>):<\/p>\n<p>[evp_embed_video url=&#8221;https:\/\/github.com\/pepaslabs\/GlitchyTable\/raw\/master\/2%20Buggy%20Solution\/2_smooth_scrolling.mp4&#8243; loop=&#8221;true&#8221; autoplay=&#8221;true&#8221; width=&#8221;376&#8243; ratio=&#8221;1.777&#8243;]<br \/>\nHowever, we have introduced a bug. If a <tt>UITableViewCell<\/tt> scrolls all the way off-screen and gets re-used before the first asynchronous <tt>_configureCell<\/tt> call has completed, a second <tt>_configureCell<\/tt> call will be queued up in the distpatch queue.<\/p>\n<p>In the case where you have both an extremely laggy model and a user whom is scrolling very aggressively, this can result in many such calls getting queued up. The result is that when the <tt>UITableView<\/tt> stops scrolling, the user will see the content of the cells cycle through all of the queued populate operations.<\/p>\n<p>To demonstrate this, we increase the simulated lag in <tt>GlitchyModel<\/tt> to 1000ms and scroll very quickly, as seen in this <a href=\"http:\/\/gfycat.com\/PleasedConfusedBluejay\" target=\"_blank\" rel=\"noopener\">video<\/a>:<\/p>\n<p>[evp_embed_video url=&#8221;https:\/\/github.com\/pepaslabs\/GlitchyTable\/raw\/master\/2%20Buggy%20Solution\/3_queued_cell_population_bug.mp4&#8243; loop=&#8221;true&#8221; autoplay=&#8221;true&#8221; width=&#8221;376&#8243; ratio=&#8221;1.777&#8243;]<\/p>\n<h2><\/h2>\n<h2>Fixing the Queued <tt>UITableViewCell<\/tt> Population Bug<\/h2>\n<p>To solve this problem, we need to ensure that multiple populate operations aren&#8217;t allowed to queue up. We can accomplish this by using a serial queue to manage our <tt>UITableViewCell<\/tt> populate operations, and ensuring that any outstanding operations are cancelled before we queue up the next operation.<\/p>\n<p>We create a trivial serial queue:<\/p>\n<pre class=\"lang:swift theme:twilight\">class SerialOperationQueue: NSOperationQueue\r\n{\r\n    override init()\r\n    {\r\n        super.init()\r\n        maxConcurrentOperationCount = 1\r\n    }\r\n}\r\n<\/pre>\n<p>We then add such a queue to each of our <tt>UITableViewCell<\/tt> instances:<\/p>\n<pre class=\"lang:swift theme:twilight\">class GlitchyTableCell: UITableViewCell\r\n{\r\n    let queue = SerialOperationQueue()\r\n}\r\n<\/pre>\n<p>Finally, we update our <tt>_configureCell<\/tt> implementation to use the operation queue:<\/p>\n<pre class=\"lang:swift theme:twilight\">private func _configureCell(cell: GlitchyTableCell, atIndexPath indexPath: NSIndexPath)\r\n{\r\n    cell.queue.cancelAllOperations()\r\n\r\n    let operation: NSBlockOperation = NSBlockOperation()\r\n    operation.addExecutionBlock { [weak operation] () -&gt; Void in\r\n\r\n        let text = self.model.textForIndexPath(indexPath)\r\n\r\n        dispatch_sync(dispatch_get_main_queue(), { [weak operation] () -&gt; Void in\r\n\r\n            if let operation = operation where operation.cancelled { return }\r\n\r\n            cell.textLabel?.text = text\r\n        })\r\n    }\r\n\r\n    cell.queue.addOperation(operation)\r\n}\r\n<\/pre>\n<p>(See also the <a href=\"https:\/\/github.com\/pepaslabs\/GlitchyTable\/tree\/master\/3%20Correct%20Solution\/GlitchyTable\" target=\"_blank\" rel=\"noopener\">Xcode project<\/a> and <a href=\"https:\/\/github.com\/pepaslabs\/GlitchyTable\/blob\/master\/3%20Correct%20Solution\/GlitchyTable\/GlitchyTable\/GlitchyTableViewController.swift\" target=\"_blank\" rel=\"noopener\">source code<\/a> of this solution).<\/p>\n<p>Now, we revisit our extremely problematic model (which simulates 1000ms lag) and verify that it behaves correctly (as seen in this <a href=\"http:\/\/gfycat.com\/LameComfortableGordonsetter\" target=\"_blank\" rel=\"noopener\">video<\/a>):<\/p>\n<p>[evp_embed_video url=&#8221;https:\/\/github.com\/pepaslabs\/GlitchyTable\/raw\/master\/3%20Correct%20Solution\/4_correct_lots_of_lag.mp4&#8243; loop=&#8221;true&#8221; autoplay=&#8221;true&#8221; width=&#8221;372&#8243; ratio=&#8221;1.777&#8243;]<br \/>\nFinally, we dial back the simulated lag to 100ms to get a sense of what this would look like in a real-world scenario (as seen in this <a href=\"http:\/\/gfycat.com\/HeavyEmbellishedIceblueredtopzebra\" target=\"_blank\" rel=\"noopener\">video<\/a>):<\/p>\n<p>[evp_embed_video url=&#8221;https:\/\/github.com\/pepaslabs\/GlitchyTable\/raw\/master\/3%20Correct%20Solution\/5_correct_small_lag.mp4&#8243; loop=&#8221;true&#8221; autoplay=&#8221;true&#8221; width=&#8221;374&#8243; ratio=&#8221;1.777&#8243;]<br \/>\nThe result is a <tt>UITableView<\/tt> UX which is tolerant of 100ms of data model lag.<\/p>\n<h2>Conclusion<\/h2>\n<p>Populating <tt>UITableViewCells<\/tt> asynchronously ensures that your <tt>UITableView<\/tt> scrolling performance is decoupled from your data model performance.<\/p>\n<p>[amp-cta id=&#8217;8486&#8242;]<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Note: This article was originally published on Github Abstract In general, the key to keeping the iPhone UI responsive is to avoid blocking the main thread. In the case of UITableView, this boils down to keeping tableView(_:cellForRowAtIndexPath:) as performant as possible. However, sometimes it is simply not possible to marshall all of the data needed&#8230;<a class=\"read-more\" href=\"https:\/\/ushipblogsubd.wpengine.com\/shipping-code\/populating-uitableviewcells-asynchronously-to-fix-uitableview-lag\/\"> Read More<\/a><\/p>\n","protected":false},"author":17,"featured_media":6410,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[295,2],"tags":[297],"class_list":["post-4259","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-shipping-code","category-company-news","tag-shipping-code"],"acf":{"blog_post_content":[{"acf_fc_layout":"blog_post_entry_footer","blog_post_entry_footer_cta":[{"blog_post_entry_footer_cta_url":"https:\/\/www.uship.com\/","blog_post_entry_footer_cta_text":"Ready to Ship Something?","blog_post_entry_footer_onclick":""}]}]},"_links":{"self":[{"href":"https:\/\/ushipblogsubd.wpengine.com\/wp-json\/wp\/v2\/posts\/4259","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ushipblogsubd.wpengine.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ushipblogsubd.wpengine.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ushipblogsubd.wpengine.com\/wp-json\/wp\/v2\/users\/17"}],"replies":[{"embeddable":true,"href":"https:\/\/ushipblogsubd.wpengine.com\/wp-json\/wp\/v2\/comments?post=4259"}],"version-history":[{"count":0,"href":"https:\/\/ushipblogsubd.wpengine.com\/wp-json\/wp\/v2\/posts\/4259\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ushipblogsubd.wpengine.com\/wp-json\/wp\/v2\/media\/6410"}],"wp:attachment":[{"href":"https:\/\/ushipblogsubd.wpengine.com\/wp-json\/wp\/v2\/media?parent=4259"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ushipblogsubd.wpengine.com\/wp-json\/wp\/v2\/categories?post=4259"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ushipblogsubd.wpengine.com\/wp-json\/wp\/v2\/tags?post=4259"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}