I've been working around the tableview and got stuck on the following issue.
As far as I understand, the willDisplayCell delegate method should allow me to access the current cell design.
My issue is: it does not work properly. That delegate function runs only five times when trying to display 80 cells. Regardless of anything other than this function, writing a line of code in the delegate function makes it work well.
Here is my code:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == comment_arr.count - 1 {
print("end")
// (1)self.comment_height.constant = self.comment_table.contentSize.height + 30
}
// (2)self.comment_height.constant = self.comment_table.contentSize.height + 30
}
If the (1) line exists, it doesn't work. But if the (2) line exists, it works well.
How do I get it to work?
First get the number of rows in section. Then find the last item
let totalRow = tableView.numberOfRows(inSection: indexPath.section)
if(indexPath.row == totalRow - 1)
{
print("end")
return
}
tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat
is called before willDisplayCell
the topic was covered in full and detailed way here:
TableView methods
Related
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
I've used this tutorial to implement pull to refresh in tableview
https://cocoacasts.com/how-to-add-pull-to-refresh-to-a-table-view-or-collection-view
Cells have dynamic height in my table.
When I pull to refresh and drag down (before I can release), table jumps. I've been reading various solutions and tried few but I couldn't fix this issue. Is there any definite solution for this problem?
I've tried following solutions:
1) This one doesn't work at all
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 70.0
}
2) https://stackoverflow.com/a/31417756/12114641
This solution seems to work however table jumps when dragging is released, it should be smooth instead of jump.
3) Following also doesn't work
UIView.animate(withDuration: 0.5) {
self.refreshControl.endRefreshing()
}
I am looking for the event that gets fired when the tableview loads the rows to fit the screen. I know tableview loads only the number of rows that fit the screen. I want to execute a set of code when the rows that fit the screen are loaded.
Any pointers on how to determine this?
I think you'd need to implement UITableViewDelegate and override the WillDisplay(UITableView, UITableViewCell, NSIndexPath) method. Be sure to set the delegate of your tableview to the class that implements UITableViewDelegate.
Unfortunately WillDisplay is called per cell, not per row.
I am not good at c#, so please translate this from Swift. I've added another solution down below which is, sort of a manual calculation of visible rows in tableview.
The Recommended Solution:
var isFirstTime:Bool = true
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let indexPaths = tableView.indexPathsForVisibleRows {
if indexPath == indexPaths.last {
if isFirstTime {
self.visibleCellsLoaded()
}
isFirstTime = false
}
}
}
The isFirstTime (Very important) flag will restrict your specific "set of code" (self.visibleCellsLoaded()) to execute only once. You can remove it if you want it to be executed every time you scroll - which apparently negates the purpose of your question.
Another solution that also works:
Here we manually calculate and get what tableView.indexPathsForVisibleRows returns us (Result is mostly similar to the previous method)
fileprivate func getLastVisibleIndexPath() -> IndexPath {
//This samples the first indexPath only, so this works only with rows that have static height, not with UITableViewAutomaticDimension.
let firstIndexPath = IndexPath.init(row: 0, section: 0)
let tableViewHeight = self.tableView.bounds.height
let rowHeight = tableView.rectForRow(at: firstIndexPath).size.height
let numberOfVisibleRows = tableViewHeight / rowHeight
return IndexPath.init(row: Int(numberOfVisibleRows - 1), section: 0)
}
var isFirstTime:Bool = true
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath == getLastVisibleIndexPath() {
if isFirstTime {
self.visibleCellsLoaded()
}
isFirstTime = false
}
}
I am trying to achieve a custom action on the last row of my UITableView.
I found a nice extension in order to know if I'm on the last row :
extension UITableView {
func isLast(for indexPath: IndexPath) -> Bool {
let indexOfLastSection = numberOfSections > 0 ? numberOfSections - 1 : 0
let indexOfLastRowInLastSection = numberOfRows(inSection: indexOfLastSection) - 1
return indexPath.section == indexOfLastSection && indexPath.row == indexOfLastRowInLastSection
}
}
The only issue I'm facing is that if my tableView has many rows, with scrollable content, the last row isn't visible.
Thus, in my cellForRow I am doing this :
if tableView.isLast(for: indexPath) {
print("Last Row - Do Specific action")
} else {
}
It is working, except that imagine my screen can display 6 rows, but the total number of rows of my tableView is 15.
It will apply my specific action on the row at indexPath 5 and 15.
I am guessing it is because each time cellForRow is called, it is checking for the last row, and it apply on the last visible row also.
How can I achieve a custom action ONLY on the last row even if it is not visible yet?
EDIT: After more investigation, the issue comes from UITableView not able to prefetch the cell, and cellLoaded and cellVisible is the same.
I can't copy paste the code as I don't have xcode right now.
But if you want to perform action before cell gets displayed then do it in
func tableView(_ tableView: UITableView,
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath)
And to check for the last row:
if indexPath.row == dataSourceArray.count - 1 {
//Perform action
}
dataSourceArray is the array from which you are fetching the data to show it in the cell.
Basically your code should be something like this
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == dataSourceArray.count - 1 {
//Perform action
}
}
You may go through apple docs to know more about tableview delegate methods
You can try with tableview delegate method:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
try this code in cell for row method
if indexPath.row == yourarray.count - 1{
// Put your code
}
I want to change the state of a cell in a TableView. I tried setting the state in "cellForRowAtIndexPath" method, but it did not work. Then I saw posts that said that in order to change the state of a cell one needs to use the delegate method "willDisplayCell". However, when I implemented the method it is not being called (I put a breakpoint which was not called)
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
cell.selected = true
}
I tried also using "willDisplay" as it appears in Apple documentation, but that did not help. As a matter of fact, I tried any garbage instead of that string and nothing happens - the app compiles with no errors but the method is not being called.
I know that the delegate of the tableView is working because other delegate methods, such as "didSelectRowAtIndexPath", are being called.
Swift 3.0 syntax for willDisplayCell is now:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
}