2 min read

Random UITableView NSRangeException

Recently I'm building a page to display long images for product descriptions. The image was split in several short images for better performance. I download the image in ProductImageCell and use closure didUpdateHeight to tell the UITableView to refresh the cell's height.

class ProductImageCell: UITableViewCell {
    
    @IBOutlet weak var productImageView: UIImageView!
    
    var didUpdateHeight: ((CGFloat, IndexPath) -> Void)?
    
    func setup(imageLink: String, indexPath: IndexPath) {
        productImageView.kf.setImage(with: URL(string: imageLink)) { (image, error, type, url) in
            if let image = image {
                let ratio = image.size.width / UIScreen.width
                let height = ceil(image.size.height / ratio)
                
                self.didUpdateHeight?(height, indexPath)
            }
        }
    }
}

With the below code to refresh the cell's height, everythings seem to be working perfectly.

cell!.didUpdateHeight = { [unowned self] height, indexPath in
    self.heightCache[indexPath] = height

    self.tableView.beginUpdates()
    self.tableView.endUpdates()
}

Then I got a bunch of crash reports from some minor iOS versions, like 10.3.1, 11.0.1, 11.0.2.

*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 4 beyond bounds [0 .. 3]'
*** First throw call stack:
(
  0   CoreFoundation                      0x000000010eafdb0b __exceptionPreprocess + 171
  1   libobjc.A.dylib                     0x000000010b8c3141 objc_exception_throw + 48
  2   CoreFoundation                      0x000000010ea32ffb -[__NSArrayM objectAtIndex:] + 203
  3   UIKit                               0x000000010c5bfd60 -[UITableView _updateVisibleCellsNow:isRecursive:] + 4676
  4   UIKit                               0x000000010c5f3ccc -[UITableView _performWithCachedTraitCollection:] + 111
  5   UIKit                               0x000000010c5dae7a -[UITableView layoutSubviews] + 233
  6   UIKit                               0x000000010c54155b -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1268
  7   QuartzCore                          0x000000011239a904 -[CALayer layoutSublayers] + 146
  8   QuartzCore                          0x000000011238e526 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 370
  9   QuartzCore                          0x000000011238e3a0 _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 24
  10  QuartzCore                          0x000000011231de92 _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 294
  11  QuartzCore                          0x000000011234a130 _ZN2CA11Transaction6commitEv + 468
  12  QuartzCore                          0x000000011234ab37 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 115
  13  CoreFoundation                      0x000000010eaa3717 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
  14  CoreFoundation                      0x000000010eaa3687 __CFRunLoopDoObservers + 391
  15  CoreFoundation                      0x000000010ea88720 __CFRunLoopRun + 1200
  16  CoreFoundation                      0x000000010ea88016 CFRunLoopRunSpecific + 406
  17  GraphicsServices                    0x0000000111bc4a24 GSEventRunModal + 62
  18  UIKit                               0x000000010c47e134 UIApplicationMain + 159
  19  AutopartsStoreLib-Demo              0x00000001095eaed0 main + 48
  20  libdyld.dylib                       0x000000010fb5b65d start + 1
  21  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

I don't have any clue what's wrong, because I wasn't making any kind of NSRangeException mistakes. But finally I pinpointed the cause by commenting code part by part.

// Crashing!!!
self.tableView.beginUpdates()
self.tableView.endUpdates()

Why is this code crashing my app? It looks like a bug in iOS. Thanks to this blog post Apple, c’mon with the docs!, I found out actually it's not a reliable way to refresh your cells' heights with beginUpdates and endUpdates. The risk is stated in Apple's doc, even it's only one line.

If you do not make the insertion, deletion, and selection calls inside this block, table attributes such as row count might become invalid.

How can I implement my feature when the go-to UI component don't work? I re-implemented the feature with UICollectionView in the end, it works well.