6

I've just implemented the library Charts (https://github.com/danielgindi/Charts) in a tableview, but I experience pretty heavy lag during scrolling, when my charts hold a lot of data.

I have a method inside the ChartTableViewCell where I draw the chart based on the data I pass and call from my viewcontroller.

func updateCell(chartData: ChartData) {

    DispatchQueue.global(qos: .background).async {
        print("This is run on the background queue")
    self.readings = (readings != nil) ? readings!.readings : []
        self.period = period

        if (chartData.isKind(of: LineChartData.self)) {
            data.lineData = chartData as! LineChartData
        }
        else if (chartData.isKind(of: BarChartData.self)) {
            data.barData = chartData as! BarChartData
        }
    }

    DispatchQueue.main.async {
    self.chartView.data = data
    }
}

In my tableViewController I call the function after parsing the data:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let meta = chartMetas[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "chartCell", for: indexPath) as! ChartTableViewCell
        cell.readings = currentReadings
        cell.updateCell()

        return cell
    }

What did I miss? Since the view is lagging so hard when scrolling.

UPDATE:

I tried, as suggested, to prepare the chart data in the viewcontroller and pass it to the cell. However it seems like the problem in the resuseable cells, the lags appears when I scroll and a new cell enters the screen. How can I fix this? It is pretty annoying.

UPDATE 2: It looks like the Charts aren't supposed to be used in a tableview... https://github.com/danielgindi/Charts/issues/3395

Recusiwe
  • 1,594
  • 4
  • 31
  • 54
  • It is unlikely from the dequeueReusableCell. That is not doing much. Are you doing anything complex in your prepareForReuse method for the cell? If not, I would see what part of the updateCell method is causing the problem by trying to stub out the parts. If the chart library fitScreenMethod() is causing the lag, you might need to do it on the background thread and display a spinner on the cell at first. – wottle Apr 17 '18 at 20:25
  • It lags even without the fitScreen()-call. As soon as a new cell is about to enter the screen, it lags and kind of jumps. It get worse when the graph has more points to show. – Recusiwe Apr 17 '18 at 20:34
  • It's still doing some heavy operations if your datasets are large. If you comment out everything from the updateCell method does it still stutter? If not, move everything in there to a background cell and update the UI once it is complete. Smooth scrolling with a slight delay when the cell appears and the chart is displayed. – wottle Apr 17 '18 at 20:36
  • If I comment out everything in updateCell() and thereby the cells contain no data, it scrolls smooth. – Recusiwe Apr 17 '18 at 20:40
  • So move all that processing to a background thread and the scrolling should remain smooth. You've proven that there is heavy processing that is triggered when you do something in updateCell (my guess is the library does some processing when you set the dataset). Simply take that processing off of the main thread that handles updating the UI (things like scrolling a tableview smoothly) and you should be good – wottle Apr 17 '18 at 20:48

3 Answers3

2

To get a serious performance boost, you'll likely need to implement UITableViewDataSourcePrefecting. By doing this, the table view will call into your delegate to let you know that a cell will be needed ahead of time. This will give your code a chance to prepare and render any data it needs.

From the documentation:

You use a prefetch data source object in conjunction with your table view’s data source to begin loading data for cells before the tableView(_:cellForRowAt:) data source method is called.The following steps are required to add a prefetch data source to your table view:

  • Create the table view and its data source.

  • Create an object that adopts the UITableViewDataSourcePrefetching protocol, and assign it to the prefetchDataSource property on the table view.

  • Initiate asynchronous loading of the data required for the cells at the specified index paths in your implementation of tableView(_:prefetchRowsAt:).

  • Prepare the cell for display using the prefetched data in your tableView(_:cellForRowAt:) data source method.

  • Cancel pending data load operations when the table view informs you that the data is no longer required in the tableView(_:cancelPrefetchingForRowsAt:) method.

picciano
  • 22,341
  • 9
  • 69
  • 82
  • I think this is assuming the problem is with the data loading. The poster has the data pre-loaded locally and ready, it is the rendering of the chart within the cell that is causing the problem. I don't believe `UITableViewDataSourcePrefetching` will help with the. – wottle Apr 17 '18 at 21:11
  • I agree. That's my point actually, you can do the rendering in the prefetch delegate. Updated answer to clarify. – picciano Apr 17 '18 at 21:21
  • Ah, gotcha. I'm not sure how the library works / whether loading the data in the prefetch will alleviate the issue. – wottle Apr 18 '18 at 01:37
0

Try to prepare all the chart data before you pass it to the cell. I mean make all the stuff you do I the updateCell() func in your tableViewController probably in viewDidLoad and pass the generated chart Datas to an array.

Then in tableView(tableView: , cellForRowAt indexPath: ) just pass the previously generated data to your cell.

So your updateCell func should looks like so:

func updateCell(lineChartData: LineChartData) {
    // SET DATA AND RELOAD
    chartView.data = lineChartData
    chartView.fitScreen()
}

And you tableView

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let meta = chartMetas[indexPath.row]
    let cell = tableView.dequeueReusableCell(withIdentifier: "chartCell", for: indexPath) as! ChartTableViewCell
    cell.readings = currentReadings
    cell.updateCell(lineChartData: arrayOfGeneratedChartData[indexPath.row])

    return cell
}

Seems that you keeps entries in the cell, so don't do this. Keep them in the tableViewController. Pass only required data to the cell and don't do hard tasks in the cell methods.

Dmitry
  • 2,963
  • 2
  • 21
  • 39
0

You chart library is probably doing some pretty heavy processing when there are large data sets. If the author of this library didn't account for this, you are executing this heavy processing on the main thread every time you scroll and a new cell appears on the screen. This will certainly cause stuttering.

I would look to change the cell setup method to simply display a spinner or placeholder table, and then kick off the loading of data and building of the chart on a background thread. This should cause the UITableView scrolling to be smooth, but you will see the cells with placeholders as you scroll through the table, where the charts get populated after the background thread processing completes.

Running things on a background thread in Swift is pretty easy.

Just make sure when you are ready to update the UI with the chart, you do that on the main thread (Al UI updates should be executed on the main thread or you will see weird visual oddities or delays in the UI being updated). So maybe hide the placeholder image and show the chart using animations on the main thread after the heavy processing is done on the background thread.

What if you did something like this?

func updateCell(chartData: ChartData) {

    DispatchQueue.global(qos: .background).async {
        print("This is run on the background queue")
    self.readings = (readings != nil) ? readings!.readings : []
        self.period = period

        if (chartData.isKind(of: LineChartData.self)) {
            data.lineData = chartData as! LineChartData
        }
        else if (chartData.isKind(of: BarChartData.self)) {
            data.barData = chartData as! BarChartData
        }     

        self.chartView.data = data

        DispatchQueue.main.async {
            // something that triggers redrawing of the chartView.
           chartView.fitScreen()
        }
    }
}

Note that the main thread call is inside the background thread execution block. This means all the stuff will run on the background thread, and when that background thread stuff is done, it will call something in the chart library that will do the UI refresh. Again, this won't help if your chart library is written to force long running operations to happen on the main thread.

wottle
  • 13,095
  • 4
  • 27
  • 68
  • I actually forgot to mention, that I've moved the heavy thing of building the dataset out of the cell, and moved it to my controller instead. So now I just pass the dataset, however it lags when the dataset is pretty damn large. – Recusiwe Apr 17 '18 at 20:38
  • You will see this same behaviour if you do anything in your cell setup that is long-running. There are many examples of `UITableView` tutorials where the tableview cell loads a thumbnail image from a network request. If you do this in your cell setup, it will cause jerky scrolling. Moving that long running network request off the main thread is the solution. Your problem is no different. – wottle Apr 17 '18 at 20:47
  • I've just updated what I tried to do in the updateCell-method, however this leaves me with the same lag. It seems that its the self.chartView.data = data, which draws the chart, that do a heavy task on the UI thread. – Recusiwe Apr 17 '18 at 23:00
  • I've just updated my original post with a link to the library issue section. – Recusiwe Apr 17 '18 at 23:07
  • Yes, but in theory if you simply move the processing to a background thread, the poor scrolling will go away. I really think you should try it, as it is a pattern that is used by hundreds of developers when a cell is populated with something that may take some time to load (whether due to network issues, or due to processing needs) – wottle Apr 18 '18 at 01:39
  • It seems like it is the Chart lib itself does the drawing of the chart on the UI thread, how can I get around that? This is the reason it lags. – Recusiwe Apr 18 '18 at 06:40
  • You could do as suggested in the library comment post and try to see if you can do the rendering of the chart in the background thread, save the image representation of the chart, and then in the cellForRowAt method, simply load the image. Without knowing how the library works, it's hard to say. Unless the library is explicitly forcing it's code on the main thread, the operations on the chart should use the thread the caller is running on. The library should be written to only do UI operations on the main thread. – wottle Apr 18 '18 at 16:45
  • you are not releasing the chart object and you are creating each time cell is setting , did you tried checking in instrumentation , issue with the objects I think, Instead of using a Controller Make a Separate class that will just create and return you a Chart View and When chart View is Returnd and you did Added , Deallocated that class and save memory, its just concept of core Animation – iOS Geek Apr 19 '18 at 05:43