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.
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:
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."