Alex
Alex

Reputation: 5278

UITableView with UITableViewCells + AutoLayout - Not As Smooth As It *Should* Be

I recently posted a question about a UITableView with custom UITableCells that was not smooth when the cell's subviews were positioned with AutoLayout. I got some comments suggesting the lack of smoothness was due to the complex layout of the cells. While I agree that the more complex the cell layout, the more calculation the tableView has to do to get the cell's height, I don't think 10-12 UIView and UILabel subviews should cause the amount of lag I was seeing as I scrolled on an iPad.

So to prove my point further, I created a single UIViewController project with a single UITableView subview and custom UITableViewCells with only 2 labels inside of their subclass. And the scrolling is still not perfectly smooth. From my perspective, this is the most basic you can get - so if a UITableView is still not performant with this design, I must be missing something.

The estimatedRowHeight of 110 used below is a very close estimate to the actual row height average. When I used the 'User Interface Inspector' and looked at the heights of each cell, one by one, they ranged from 103 - 124.

Keep in mind, when I switch the code below to not use an estimatedRowHeight and UITableViewAutomaticDimension and instead implement func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {, calculating the height with frame values, the UITableView scrolls like butter.

Screenshot of App (for reference)

enter image description here

Source code of the App (where the scrolling is not perfectly smooth)

// The custom `Quote` object that holds the
// properties for our data mdoel
class Quote {
    var text: String!
    var author: String!

    init(text: String, author: String) {
        self.text = text
        self.author = author
    }
}


// Custom UITableView Cell, using AutoLayout to
// position both a "labelText" (the quote itself)
// and "labelAuthor" (the author's name) label
class CellQuote: UITableViewCell {
    private var containerView: UIView!
    private var labelText: UILabel!
    private var labelAuthor: UILabel!


    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        contentView.backgroundColor = UIColor.whiteColor()

        containerView = UIView()
        containerView.backgroundColor = UIColor(
            red: 237/255,
            green: 237/255,
            blue: 237/255,
            alpha: 1.0
        )
        contentView.addSubview(containerView)
        containerView.translatesAutoresizingMaskIntoConstraints = false
        containerView.leadingAnchor.constraintEqualToAnchor(contentView.leadingAnchor, constant: 0).active = true
        containerView.trailingAnchor.constraintEqualToAnchor(contentView.trailingAnchor, constant: 0).active = true
        containerView.topAnchor.constraintEqualToAnchor(contentView.topAnchor, constant: 4).active = true
        containerView.bottomAnchor.constraintEqualToAnchor(contentView.bottomAnchor, constant: 0).active = true

        labelText = UILabel()
        labelText.numberOfLines = 0
        labelText.font = UIFont.systemFontOfSize(18)
        labelText.textColor = UIColor.darkGrayColor()

        containerView.addSubview(labelText)
        labelText.translatesAutoresizingMaskIntoConstraints = false
        labelText.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor, constant: 20).active = true
        labelText.topAnchor.constraintEqualToAnchor(containerView.topAnchor, constant: 20).active = true
        labelText.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor, constant: -20).active = true

        labelAuthor = UILabel()
        labelAuthor.numberOfLines = 0
        labelAuthor.font = UIFont.boldSystemFontOfSize(18)
        labelAuthor.textColor = UIColor.blackColor()

        containerView.addSubview(labelAuthor)
        labelAuthor.translatesAutoresizingMaskIntoConstraints = false
        labelAuthor.topAnchor.constraintEqualToAnchor(labelText.bottomAnchor, constant: 20).active = true
        labelAuthor.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor, constant: 20).active = true
        labelAuthor.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor, constant: -20).active = true
        labelAuthor.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor, constant: -20).active = true

        self.selectionStyle = UITableViewCellSelectionStyle.None
    }

    func configureWithData(quote: Quote) {
        labelText.text = quote.text
        labelAuthor.text = quote.author
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

// The UIViewController that is a 
class ViewController: UIViewController, UITableViewDataSource {

    var tableView: UITableView!
    var dataItems: [Quote]!

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView = UITableView()
        tableView.dataSource = self

        tableView.registerClass(CellQuote.self, forCellReuseIdentifier: "cellQuoteId")

        tableView.backgroundColor = UIColor.whiteColor()
        tableView.separatorStyle = UITableViewCellSeparatorStyle.None
        tableView.estimatedRowHeight = 110
        tableView.rowHeight = UITableViewAutomaticDimension

        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor).active = true
        tableView.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 20).active = true
        tableView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor).active = true
        tableView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor).active = true

        dataItems = [
            Quote(text: "One kernel is felt in a hogshead; one drop of water helps to swell the ocean; a spark of fire helps to give light to the world. None are too small, too feeble, too poor to be of service. Think of this and act.", author: "Michael.Frederick"),
            Quote(text: "A timid person is frightened before a danger, a coward during the time, and a courageous person afterward.", author: "Lorem.Approbantibus."),
            Quote(text: "There is only one way to defeat the enemy, and that is to write as well as one can. The best argument is an undeniably good book.", author: "Lorem.Fruitur."),
            // ... many more quotes ...
        ]

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    // MARK: - UITableViewDataSource

    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataItems.count
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cellQuoteId") as! CellQuote
        cell.configureWithData(dataItems[indexPath.row])
        return cell
    }
}

I like matt's suggestion below, but am still trying to tweak it to work for me:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    var cellHeights: [CGFloat] = [CGFloat]()

    func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        if cellHeights.count == 0 {
            var cellHeights = [CGFloat]()
            let numQuotes: Int = dataItems.count

            for index in 0...numQuotes - 1 {
                let cell = CellQuote()
                let quote = dataItems[index]

                cell.configureWithData(quote)
                let size = cell.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
                cellHeights.append(size.height)
            }

            self.cellHeights = cellHeights
        }

        return self.cellHeights[indexPath.row]

    }
}

Upvotes: 3

Views: 576

Answers (2)

Casey
Casey

Reputation: 6701

the clear background from your two text labels is causing the performance issues. add these lines and you should see a performance increase:

labelText.backgroundColor = containerView.backgroundColor
labelAuthor.backgroundColor = containerView.backgroundColor

a good way to check any other potential issues is by turning on 'Color Blended Layers' in the iOS Simulator's 'Debug' menu option

UPDATE

usually what i do for dynamic cell heights is create a prototype cell and use it for sizing. here is what you'd do in your case:

class CellQuote: UITableViewCell {
    private static let prototype: CellQuote = {
        let cell = CellQuote(style: .Default, reuseIdentifier: nil)
        cell.contentView.translatesAutoresizingMaskIntoConstraints = false
        return cell
    }()

    static func heightForQuote(quote: Quote, tableView:UITableView) -> CGFloat {
        prototype.configureWithData(quote)
        prototype.labelText.preferredMaxLayoutWidth = CGRectGetWidth(tableView.frame)-40
        prototype.labelAuthor.preferredMaxLayoutWidth = CGRectGetWidth(tableView.frame)-40

        prototype.layoutIfNeeded();
        return CGRectGetHeight(prototype.contentView.frame)
    }

    // existing code here
}

in your viewDidLoad remove the rowHeight and estimatedRowHeight lines and replace with becoming the delegate

class ViewController {
    override func viewDidLoad() {
        // existing code

        self.tableView.delegate = self

        // existing code
    }

    // get prototype cell height
    func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        let quote = dataItems[indexPath.row]
        return CellQuote.heightForQuote(quote, tableView: tableView)
    }

Upvotes: 2

matt
matt

Reputation: 536019

I've never found the automatic row height mechanism to be as smooth as the old calculated layout techniques that we used to use before auto layout came along. The bottleneck, as you can readily see by using Instruments, is that the runtime must call systemLayoutSizeFittingSize: on every new cell as it scrolls into view.

In my book, I demonstrate my preferred technique, which is to calculate the heights for all the cells once when the table view first appears. This means that I can supply the answer to heightForRowAtIndexPath instantly from then on, making for the best possible user experience. Moreover, if you then replace your call to dequeueReusableCellWithIdentifier with the much better and more modern dequeueReusableCellWithIdentifier:forIndexPath, you have the advantage that the cell comes to you with its correct size and no further layout is needed after that point.

Upvotes: 2

Related Questions