acb's technical journal

Posts matching tags 'workaround'

2015/5/25

When self-sizing UITableView cells don't

One of the advances in iOS 8 was self-sizing UITableView cells. In earlier versions of iOS, if one wanted a table view whose cells' height varied, one had to explicitly calculate the height for each cell, providing a method which returned it; a process involving considerable string-measuring. Since iOS 8, this is no longer necessary; it is now possible to set a UITableView's rowHeight to a new magic value, UITableViewAutomaticDimension; this causes the table view to evaluate each cell's auto-layout constraints, determining its size. Of course, this means that one has to design one's custom cells in such a way that the constraints “push outwards” on the top and bottom edges of the content view, expanding it as the intrinsic sizes of the contained subviews increase. Still, that makes auto-sizing table view cells a lot simpler, especially for complex cells; in theory, at least.

This mechanism is not without its problems. There is an intermittent bug which, in some instances, causes cells to not be sized until the second time they are shown. In other words, when the cell first appears, it is in the default size it is on the storyboard, and any text that does not fit on labels is truncated. If the user scrolls the cell off the screen and then scrolls back to it, it reappears properly sized. Or, taking an example (taken from my MPDluxe MPD controller app):

The above view is a UITableView showing the contents of a directory on a music server. The left image is what appears when the user first enters the directory; while the second cell is sized correctly, the third cell is not, and the title is cut off. The right image is what appears if the user scrolls down (moving the top cells off the screen) and then scrolls back up; here, the third cell is sized properly.

Given that a second displaying operation causes the cell's size to be calculated properly, one workaround may be to force the cells to be reloaded when the view appears; i.e., adding to the view controller:

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        self.tableView.reloadData()
    }
It turns out that this doesn't work; reloadData is called too early, and is coalesced into the initial load, having no effect. So let's add a delay:
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        dispatch_after(
            dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC))),
            dispatch_get_main_queue()) {
                self.tableView.reloadData()
        }
    }
That's better; the cells that appear at the start are now sized correctly. However, if there are any mis-sized cells below the first screenful, they will still appear incorrectly sized the first time the user scrolls to them. Which means that we need something subtler than reloading the whole table view at start time.

Fortunately for us, the UITableViewDelegate protocol has an optional method titled tableView:willDisplayCell:forRowAtIndexPath:. As the name suggests, if defined, this is called just before a cell is displayed. Which would allow us to force a reload of the cell.

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    dispatch_after(
        dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC))),
        dispatch_get_main_queue()) {
             tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
        }
}
Which does the trick; the cells appear with the correct size now. However, scrolling is also noticeably slower. The problem is, each cell is reloaded every time it appears, whether it has been resized or not. So the next step is to add a memory of which cells have been reloaded, and not reload any cell more than once:
var alreadyReloaded: Set = Set()

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    alreadyReloaded = Set()
}

...

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    // we assume that there will never be more than 2^16 sections or rows in a section
    let index = indexPath.row | (indexPath.section << 16)
    if(!alreadyReloaded.contains(index)) {
        alreadyReloaded.insert(index)
        dispatch_after(
            dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC))),
            dispatch_get_main_queue()) {
                tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
            }
    }
}
This will cause each cell to be reloaded only once.

Of course, this is just a beginning; if the user scrolls down rapidly, it will still trigger a flurry of reloads. The next step would involve application-specific logic, checking whether a cell can be eliminated as a candidate for reloading based on model information (for example, is it a type of cell which suffers from the problem, or is the information in the model particularly long).

autolayout ios uitableview workaround 0

This will be the comment popup.
Post a reply
Display name:

Your comment:


Please enter the text in the image above here: