Home > Software engineering >  TableView freeze
TableView freeze

Time:12-22

I have an array of items and TableView to display them. Item consists of 4 properties. And there is a method which randomly generate items. Initialy, array is empty but in viewDidLoad i have method, which append 100 items to array with delay about 1 second. Array appends until its count about 1_000_000 items. When I start app it freeze.

Generating items method:

func subscribeToDeals(callback: @escaping ([Deal]) -> Void) {
    queue.async {
      var deals: [Deal] = []
      let dealsCount = Int64.random(in: 1_000_000..<1_001_000)
      let dealsCountInPacket = 100
      var j = 0
      for i in 0...dealsCount {
        let currentTimeStamp = Date().timeIntervalSince1970
        let timeStampRandomizer = Double.random(in: 50_000...50_000_000)
        let deal = Deal(
          id: i,
          dateModifier: Date(timeIntervalSince1970: Double.random(in: currentTimeStamp - timeStampRandomizer...currentTimeStamp)),
          instrumentName: self.instrumentNames.shuffled().first!,
          price: Double.random(in: 60...70),
          amount: Double.random(in: 1_000_000...50_000_000),
          side: Deal.Side.allCases.randomElement()!
        )
        deals.append(deal)
        
        j  = 1
        
        if j == dealsCountInPacket || i == dealsCount {
          j = 0
          let delay = Double.random(in: 0...3)
          let newDeals = deals
          DispatchQueue.main.asyncAfter(deadline: .now() delay) {
            callback(newDeals)
          }
          deals = []
        }
      }
    }
  }

It's my tableView methods:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return model.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: DealCell.reuseIidentifier, for: indexPath) as! DealCell
        guard model.count != 0 else {
            cell.instrumentNameLabel.text = "no data"
            cell.priceLabel.text = "no data"
            cell.amountLabel.text = "no data"
            cell.sideLabel.text = "no data"
            return cell
        }
        cell.instrumentNameLabel.text = "\(model[indexPath.row].instrumentName)"
        cell.priceLabel.text = "\(model[indexPath.row].price)"
        cell.amountLabel.text = "\(model[indexPath.row].amount)"
        cell.sideLabel.text = "\(model[indexPath.row].side)"
        return cell
    }

Function to append array:

server.subscribeToDeals { deals in
            self.model.append(contentsOf: deals)
            self.tableView.reloadData()
        }

How to solve this problem? May be to make some counter for "numberOfRowsInSection" equal to 100 and then when scroll to 100th item increase it to 200 etc. Or is there a more concise solution?

Tried to use ReusableCell, but nothing happened. There is from CPU profiler

Zombie

CodePudding user response:

The code below should solve your problem by unblocking the main thread using DispatchQueue

server.subscribeToDeals { deals in
       self.model.append(contentsOf: deals)
       DispatchQueue.main.async {
          self.tableView.reloadData()
       } 
    }

CodePudding user response:

I don't know what you are really trying to do here... your code seems like it's just doing a "stress test" or something.

However, to try and help you understand why your app is "freezing" --

Your for i in 0...dealsCount { loop will be running very fast. As in maybe 1 or 2 thousandths of a second per 100 iterations. If you are calling .reloadData() every 100th time through the loop, your code is trying to update the UI pretty much constantly. That means your UI will appear "frozen."

Here's an example that you may find helpful...

First, we'll add a "status label" and a progress view to display the progress as we generate Deals. We'll update those every 1000th new Deal created.

Second, as we generate new Deals, we'll append them directly to the controller's var model: [Deal] = [] array (instead of building new arrays and appending them periodically).

Third, we'll only call .reloadData():

  • at the first 1,000 Deals
  • then at every 100,000 Deals
  • and finally after we've generated all 1-million

As I said, I don't know what you're really doing ... but it is unlikely someone would scroll through the first 1,000 rows before we add the next 100,000 records.

However, you'll find that you can scroll the table while the records are being generated... and, after they've all been generated, selecting any cell will jump to the 900,000th row.

Here's how it looks:

enter image description here enter image description here

enter image description here enter image description here

Deal Struct

struct Deal {
    var id: Int64 = 0
    var dateModifier: Date = Date()
    var instrumentName: String = ""
    var price: Double = 0
    var amount: Double = 0
}

Simple multi-line label cell class

class DealCell: UITableViewCell {
    let theLabel = UILabel()
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        theLabel.numberOfLines = 0
        theLabel.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        theLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(theLabel)
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            theLabel.topAnchor.constraint(equalTo: g.topAnchor),
            theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            theLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
    }
    func fillData(_ aDeal: Deal) {
        let i = aDeal.id
        let d = aDeal.dateModifier
        let nm = aDeal.instrumentName
        let p = String(format: "%0.2f", aDeal.price)
        let a = String(format: "%0.2f", aDeal.amount)
        theLabel.text = "\(i): \(nm)\nPrice: \(p) / Amount: \(a)\n\(d)"
    }
}

Demo view controller

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    var model: [Deal] = []
    
    let instrumentNames: [String] = [
        "accordion",
        "acoustic guitar",
        "bagpipes",
        "banjo",
        "bass guitar",
        "bongo drums",
        "bugle",
        "cello",
        "clarinet",
        "cymbals",
        "drums",
        "electric guitar",
        "flute",
        "French horn",
        "harmonica",
        "harp",
        "keyboard",
        "maracas",
        "organ",
        "pan flute (pan pipes)",
        "piano",
        "recorder",
        "saxophone",
        "sitar",
        "tambourine",
        "triangle",
        "trombone",
        "trumpet",
        "tuba",
        "ukulele",
        "violin",
        "xylophone",
    ]
    
    let tableView = UITableView()
    let progressView = UIProgressView()
    let statusLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        [statusLabel, progressView, tableView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([

            statusLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
            statusLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
            statusLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            
            progressView.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 8.0),
            progressView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            progressView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            tableView.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 8.0),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),

        ])

        tableView.register(DealCell.self, forCellReuseIdentifier: "c")
        tableView.dataSource = self
        tableView.delegate = self
        
        statusLabel.textAlignment = .center
        statusLabel.textColor = .systemRed
        
        subscribeToDeals()
    }
    
    func updateProgress(currentCount: Int64, futureCount: Int64) {
        
        // update the status Lable and progress bar
        statusLabel.text = "Generated \(currentCount) of \(futureCount)"
        progressView.progress = Float(currentCount) / Float(futureCount)

        // only reload the table if we're at
        //  the first 1_000, or
        //  every 100_000, or
        //  we're finished generating Deals
        
        // if it's the first update
        if currentCount == 1_000 {
            tableView.reloadData()
        }
        // else if we're at an even 100_000
        else if currentCount % 100_000 == 0 {
            tableView.reloadData()
        }
        // else if we've generated all
        else if currentCount == futureCount {
            tableView.reloadData()
        }
        
    }

    func subscribeToDeals() {
        
        let bkgQueue = DispatchQueue(label: "subscribing", qos: .background)
        
        bkgQueue.async{
            
            let dealsCount = Int64.random(in: 1_000_000..<1_001_000)

            for i in 0...dealsCount {
                
                let currentTimeStamp = Date().timeIntervalSince1970
                let timeStampRandomizer = Double.random(in: 50_000...50_000_000)
                let deal = Deal (
                    id: i,
                    dateModifier: Date(timeIntervalSince1970: Double.random(in: currentTimeStamp - timeStampRandomizer...currentTimeStamp)),
                    instrumentName: self.instrumentNames.shuffled().first!,
                    price: Double.random(in: 60...70),
                    amount: Double.random(in: 1_000_000...50_000_000)
                )
                
                // append directly to data
                self.model.append(deal)
                
                // if we're at a 1_000 point
                if i % 1_000 == 0 {
                    DispatchQueue.main.async {
                        self.updateProgress(currentCount: i, futureCount: dealsCount)
                    }
                }

            }

            // we've generated all deals
            DispatchQueue.main.async {
                self.updateProgress(currentCount: dealsCount, futureCount: dealsCount)
            }

            print("Done generating \(dealsCount) Deals!")
        }
        
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return model.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! DealCell
        c.fillData(model[indexPath.row])
        return c
    }
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if model.count > 1_000_000 {
            tableView.scrollToRow(at: IndexPath(row: 900_000, section: 0), at: .middle, animated: false)
        }
    }
}

Note: this is Example Code Only!!! It is not intended to be, and should not be considered to be, "production ready."

  • Related