iOS UITableView with several performBatchUpdates called relatively quickly causes scrolling to be jerky

I have an app using UITableView where an array data is first fetched from a third party api, then each item of the array data needs some local processing which takes a bit of time, then that processed data needs to be inserted into the UITableView. How long each item of the array needs for processing will vary a lot. Once the table reaches the bottom, more data is fetched and the same process occurs again.

The reason I am doing the processing on each item of the array data and adding it one by one instead of processing all of it at once and inserting it all at once is because this allows me to show at least some of the data to the user right away instead of the user waiting for a few seconds before all the data appears.

I have simulated my scenario with the following simple code without any API:

import UIKit
import SnapKit

struct Record : Codable {
    var title : String?
    var detail : String?
    
    func titleAttributedText() -> NSAttributedString? {
        guard let t = title else {
            return nil
        }
        return NSAttributedString(string: t, attributes: [.foregroundColor : UIColor.white, .font : UIFont.systemFont(ofSize: 20, weight: .bold)])
    }
    
    func detailAttributedText() -> NSAttributedString? {
        guard let d = detail else {
            return nil
        }
        return NSAttributedString(string: d, attributes: [.foregroundColor : UIColor.darkGray, .font : UIFont.systemFont(ofSize: 14, weight: .regular)])
    }
}

class ViewController: UIViewController, UITableViewDataSource {

    var tableView = UITableView()
    var records = [Record]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.dataSource = self
        view.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        fetchRecords()
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return records.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let reuseIdentifier = "cell"
        let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) ?? UITableViewCell(style: .subtitle, reuseIdentifier: reuseIdentifier)
        
        let record = records[indexPath.row]
        
        cell.textLabel?.numberOfLines = 0
        cell.detailTextLabel?.numberOfLines = 0
        cell.textLabel?.attributedText = record.titleAttributedText()
        cell.detailTextLabel?.attributedText = record.detailAttributedText()
        
        if indexPath.row > records.count - 5 && !fetching {
            fetchRecords()
        }
        
        return cell
    }
    
    var fetching = false
    let group = DispatchGroup()
    let queue = DispatchQueue(label: "delayer", attributes: .concurrent)
    
    func fetchRecords(){
        fetching = true
        print("fetchRecords")
        DispatchQueue.global().async {
            for _ in 0...30 {
                let timeout: TimeInterval = Double(arc4random_uniform(10)) / 100 // 0 to 0.1 seconds delay
            
                self.group.enter()
                
                self.queue.asyncAfter(deadline: .now() + timeout) {
                    self.group.leave()
                }
                
                _ = self.group.wait(timeout: .distantFuture)
                
                
                DispatchQueue.main.async {
                    self.tableView.performBatchUpdates {
                        self.records.append(Record(title: "New data \(self.records.count + 1)", detail: "Delay: \(timeout)"))
                        self.tableView.insertRows(at: [IndexPath(row: self.records.count-1, section: 0)], with: .automatic)
                    }
                }
                
            }
            self.fetching = false
        }
    }
}

My problem is that when the self.tableView.performBatchUpdates is occurring, the scrolling becomes choppy. When you scroll towards the end of the list and new data is being inserted, the scrolling is choppy and frame rate drops. The above code is able to demonstrate it. In my real app, it's a lot worse.

Here's a video recording:

https://i.imgur.com/Sy9iTFD.mp4

I have used Instruments Time Profiler and I see the following:

enter image description here

As you can see, the _performBatchUpdates is taking up a lot of time.

I am not sure how to fix it. Any suggestions?

EDIT:

I have found a temporary workaround by debouncing the calls to performBatchUpdates by about 500ms. So instead of constantly spamming the performBatchUpdates, I add the new records to a temporary array, then debounce a function which inserts the new records such that consecutive performBatchUpdates aren't called in less than 500ms. With this said, I would still like a better solution of any.

Upvotes: 2

Views: 79

Answers (0)

Related Questions