I want the UITableView
's refreshing to be triggered by a new cell's appearing.
Simple setup. UITableView
, dataSource
is [Double]
. Each row in the table will present one number in the array.
The special thing I wanna do is that I want the table to label the cell with max number of all visible cells. Then each of the other cells will calculate the difference from the max number onscreen. And this should update every time a new cell appears.
import UIKit
class ViewController: UIViewController {
var max = 0.0
var data:[Double] = [13,32,43,56,91,42,26,17,63,41,73,54,26,87,64,33,26,51,99,85,57,43,30,33,20]
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.reloadData()
}
private func calculate() {
// calculate the max of visible cells
let indexes = tableView.indexPathsForVisibleRows!
max = indexes
.map { data[$0.row] }
.reduce(0) { Swift.max($0,$1) }
// trying to update all the visible cells.
// tableView.reloadRows(at: indexes, with: .none) ****
// tableView.reloadData() ****
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "default")!
cell.textLabel?.text = "\(data[indexPath.row]) : \(max)"
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 64
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
calculate()
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
calculate()
}
}
What I Have Done
There are 2 lines of code with marks of **** at the end.
If I uncomment tableView.reloadRows(at: indexes, with: .none)
, Xcode produces error: 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndexedSubscript:]: index 15 beyond bounds [0 .. 13]'
. I don't really understand why, but I know onscreen visible cells are at most 14 in order to fit in the screen, but in table loading phase, the simulator thinks there were 19 visible cells at some point
If instead, I uncomment tableView.reloadData()
, this lines of code will trigger willDisplay
func which will again call calculate
func which has reloadData()
in it. It was a infinite recursive cycle, and no row successfully displayed onscreen.
If instead, I don't uncomment anything, the table will not update cells that are already onscreen. Only newly appearing cells will correctly display the effect that I want.
Thank you for reading all this and trying to offer help.
You don't want to call reloadCells
since that will trigger willDisplay
and then you get an infinite loop.
Rather, you can access the visible cell and update it directly by calling cellForRow(at:)
for the visible cells.
private func updateVisibleCells() {
for indexPath in self.tableView.indexPathsForVisibleRows ?? [] {
if let cell = self.tableView.cellForRow(at: indexPath) {
cell.textLabel?.text = "\(data[indexPath.row]) : \(max)"
}
}
}
Now, if you call updateVisibleCells
straight after you call calculate()
in willDisplayCell
you will get a message in the console:
Attempted to call -cellForRowAtIndexPath: on the table view while it was in the process of updating its visible cells, which is not allowed.
This will cause a crash in a release build.
To work around this we can defer the call to updateVisibleCells
by dispatching it asynchronously so that it is called after the table view update is complete:
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
self.calculate()
DispatchQueue.main.async {
self.updateVisibleCells()
}
}
You want to use didEndDisplaying
rather than willDisplay
so that the rows are updated correctly when rows scroll out of view. You don't need anything in willDisplay