▶hlongvu

Caching Views for UITableview Smooth Scrolling

Making UITableView scrolling smooth gain with view caching, for heavy loaded views.

Recently I’ve been making an iOS app required to add multiple views into one screen. The approach is clearly building the screen by UITableview. On top of my views is a Chart, which is from https://github.com/danielgindi/Charts. That’s an excellent library, provides you with almost all kind of Charts you need.

As you see in the above images, the Chart contains up to 11 LineChartEntry, each of which has an enormous amount of points and values. All of this view is put into an UITableViewCell.

Loading Cell into UITableView

To load this Chart into table view, we normally had it at cellForRowAt: indexPath callback of UITableViewDelegate. Inside this function, we init the cell and set Chart data:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {      
        let cell = tableView.dequeueReusableCell(withIdentifier: m.nibName(), for: indexPath)      
        if let chartCell = cell as? ChartCell{
                chartCell.fillData(data: Data)
        }
        return cell
    }

This fillData function will prepare the data for lineChart, then apply it to the chart.

lineChart?.dataSet = dataSet // heavy processing here

This approach working well, the Chart showed up. But when scrolling up and down the screen, we noticed a small lagging. And that lag was easy to understand, every time we scroll over and back to this ChartCell, the cellForRowAt: indexPath callback hit again and re-calculate and re-render this Cell.

The heaviest part of rendering this cell was when lineChart?.dataSet = dataSet got called. The Chart is a very complex UIView, and it will render all over again the entire chart.

Can We render the view by another thread

My first thought is trying to put m.fillData(data: Data) into another thread. That would reduce the processing for main UI thread. But that was a mistake:

DispatchQueue.global(qos: .background).async{
     chartCell.fillData(data: Data)
}

We got this error:

Main Thread Checker: UI API called on a background thread: -[UIView initWithFrame:]
PID: 36921, TID: 506039, Thread name: (none), Queue name: com.apple.root.background-qos, QoS: 9

So the chart couldn’t be set data from a background thread. We’ll try another way.

Generate an image of the chart

After searching around I have found some good info on Chart embed inside UITableView

  1. https://github.com/danielgindi/Charts/issues/2888
  2. https://github.com/danielgindi/Charts/issues/3699
  3. https://github.com/danielgindi/Charts/issues/3395

So the suggestion here is generating an image of the Chart then place on the UITableViewCell.

Chart library provide a way to get this:

lineChart?.getChartImage(transparent: Bool)

We can solve the lagging by this approach, generate the chart image then using it to display on the UITableView. But remember, the process of generating this image should not be placed on cellForRowAt: indexPath callback or the same lagging will happen again.

Better? Instantiate the Chart before UITableView

The Image approach has some problem like we couldn’t interact with the Chart at all. Remember that in order to get the image we also create a chart before that. There is no shortcut to get that Image. So why we don’t cache this chart instead of the image and pass it to the UITableView?

So this is our new approach:

class MyVC: UIViewController {
    var lineChart: LineChartView?
    func createChart(data: Data){
       lineChart = LineChartView(frame: frame)
        lineChart?.dataSet = generateDataSet(data)
    }
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {      
        let cell = tableView.dequeueReusableCell(withIdentifier: m.nibName(), for: indexPath)      
        if let chartCell = cell as? ChartCell{
                chartCell.setChart(self.lineChart, size: size)
        }
        return cell
}

In the UITableView Delegate, we simply add the Chart to this UITableViewCell.contentView, no more heavy processing in this function:

class ChartCell: UITableViewCell{
 func setChart(_ lineChartView: LineChartView?, size: CGSize){      
        if lineChartView?.superview == self.contentView{
            print("already in view")
        }else{
            print("not in view")
            if let chart = lineChartView{
                if (chart.superview != nil) {
                    chart.removeFromSuperview()
                }
                 self.contentView.removeAllChild()                
                self.contentView.addSubview(chart)
                chart.translatesAutoresizingMaskIntoConstraints = false
                
                chart.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
                chart.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
                chart.widthAnchor.constraint(equalToConstant:size.width).isActive = true
                chart.heightAnchor.constraint(equalToConstant: size.height).isActive = true
            }
        }
    }

Conclusion

By caching view for UITableView, we can reduce the lagging and make scrolling smooth again. I have implemented this approach in my apps and it works very well. Also, check out my app on the Appstore: Coin Price - Crypto Market Cap