UITableView calling didEndDisplayingCell incorrectly - ios

I am having an issue using a UITableView's didEndDisplayingCell method. I am using this method along with willDisplayCell to handle some extra data tracking for my table. The problem is that didEndDisplayingCell is being called several times in a row, and at times where the cell is actually still being displayed. I cannot figure out why this would happen.
More Details
I am using an NSFetchedResultsController to provide data for the table. Changes come into the table from an NSManagedObjectContext on a different queue and then get merged into the NSManagedObjectContext that the table uses. This all seems to be working fine. Looking at my implementation of the above methods:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
debugPrint("willDisplay cell at \(indexPath).")
self.dataProvider.addTrackedRelatedRecords(for: indexPath)
}
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
debugPrint("didEndDisplaying cell at \(indexPath).")
self.dataProvider.removeTrackedRelatedRecords(for: indexPath)
}
The output I'm seeing here is:
"Updating rows in table at [0, 0]". <=== This is an update to the data
"willDisplay cell at [0, 0]." <=== This is what I expect
"didEndDisplaying cell at [0, 0]"
"didEndDisplaying cell at [0, 0]"
"didEndDisplaying cell at [0, 0]"
The cell remains visible in the table and never disappears. There is nothing that I can see that would cause the didEndDisplaying to be called, especially repeatedly.
I can work around this issue by checking to see if the cell is actually still visible, by adding the following to the didEndDisplayingCell method:
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
debugPrint("didEndDisplaying cell at \(indexPath).")
// WORK AROUND
if let visibleRows = tableView.indexPathsForVisibleRows, visibleRows.contains(indexPath) {
return
}
self.dataProvider.removeTrackedRelatedRecords(for: indexPath)
}
This correctly sees that the cell is still visible, so won't remove my extra tracking. But, if the cell is visible, why is the method being called at all?
Obviously there is a lot of other code at work in my app. I will attempt to cull this down to a very simple case, but thought I'd post this first to see if anyone has seen cases where this might happen. That might help me track it down.
Thanks for any insight.

I found the issue and I'm posting an answer in case anyone else sees this issue and would be helped by what I found.
It turns out that I had inadvertently left a tableView.reloadRows(at: [indexPath], with: .fade) inside a tableView.beginUpdates() and tableView.endUpdates() block.
I believe that the reloadRows was causing the cell to be replaced, which would cause a didEndDisplayingCell. And, since there were a couple updates happening inside the block, these were being queued up and posted once the tableView.endUpdates() was called.
Removing the reloadRows call fixed the issue and all is working as expected.

In my case it was because I was calling reloadData() whenever I was changing the centeredIndexPath. Make sure you are not reloading the data as all the cells will call didEndDisplaying (as you are reloading it).

Related

In iOS, how can I detect if a cell in UITableview is in the process of being swiped?

I'm using leadingSwipeActionsConfigurationForRowAt to detect and process swipes. However, I need to be able to detect when the user started to swipe, but before it completes. The problem is that if I update the table using reloadData() or beginUpdates() / endUpdates() while a cell is being swiped, it resets the cell to the un-swiped position (and I don't like that kind of user experience). I want to be able to delay the cell/data update until the swipe is finished or cancelled.
Even though Andreas' answer is absolutely correct, I found an easier way to do it. Apparently, whenever leadingSwipeActionsConfigurationForRowAt is used, the cell is marked as being edited whenever it starts being swiped. And that allows us to use willBeginEditingRowAt and didEndEditingRowAt on the UITableView, without having to worry about the UITableViewCell
var swipedRow: IndexPath? = nil
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
swipedRow = indexPath
}
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
swipedRow = nil
}
And then you can just check if swipedRow == nil (plus, if it's not nil, you will know the indexPath of the row being swiped)
You could implement willTransition(to:) in the UITableViewCell` to detect a swipe, see:
UITableViewCell documentation
There, just set a flag in the view controller to prevent updates.
You might also need to do something in viewWillDisappear or so, because maybe the transition callbacks are not called under some circumstances.

How to determine when the cells in a tableView are returned so as to add a uiview after its finished?

I'm working with some legacy code and it looks like they added a custom sticky header view in the viewDidLoad. The issues is that I need to add the sticky header view AFTER the other cells are returned so that I may send the sticky header view to the back of the other cells so it can be used as a background for the topmost cell.
like so:
homeScreenTableView.sendSubviewToBack(headerView)
or
homeScreenTableView.insertSubview(headerView, at: 0)
The issue is there is no call back after the cells are returned in a tableView. I know this is a bit hacky, but is there a way to do this?
You check if the last indexPath in indexPathsForVisibleRows array has been displayed on screen.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last {
if indexPath == lastVisibleIndexPath {
// finished loading
}
}
}

Use tableView willDisplay cell forRowAt indexPath for custom Cells (with custom height)

In my project I have a UITableView displaying custom cells of a fixed height.
When I segue to the ViewController, I want to run an animation in the tableViewCells. when i scroll down, and then back to the first cell I do not want to run the animation again.
To achieve this I am using tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath). I then use an array to remember which cells have been on the screen already.
My function looks like this:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let myCell = cell as! myTableViewCell
if !alreadyShown.contains(indexPath) {
alreadyShown.append(indexPath)
myCell.animate()
} else {
myCell.dontAnimate()
}
}
However, this seems to ignore the height of my cell. Therefore it runs for every indexPath that would have been displayed for standard cells (e.g. 17 times on an iPhone X), even though 14 of those 17 cells are not visible.
When i scroll down later, the animation is already finished for those 14 next cells.
Is there any way of telling the willDisplay cell function the cell height so that it knows which cells are actually going to be displayed? Or is there any other way of keeping score of which cells have actually been displayed already?
The solution was setting the estimatedRowHeight for the tableView to a number equal to or greater than the actual hight of the cells in viewDidLoad. This made sure that tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) is only called on the actually visible cells.

How to make row selected when moving between multiple views

I am working on xamarin.ios. I have a UITableView control at my view. I select a row in it and then move to next screen. If I come back to UITableView screen again the selected row doesn't highlight. It highlight for a second and then deselect automatically. How it can be managed if I come back to the tableview, the selected row should be highlighted.
Hard to say without seeing any code, but looks like the tableview is being reloaded in viewDidAppear. You may want to store the selected row index in NSUserDefaults or somewhere else to persist the selection between view loads/appearances.
Make sure you are setting it selected after the tableview reloads as well, using an appropriate delegate method. Again, without any code to look at, it's hard to see where - and which order - you're doing this, but an example (Swift):
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row == tableView.numberOfRowsInSection(0) - 1 {
cell.selected = true
}
}
Alternatively, you could set the selected property in your datasource directly, and then you could do:
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
datasource[indexPath.row].selected == true {
cell.selected = true
}
}
If you do it this way, then the selected row will always be set correctly every time the tableview is loaded.
(Full disclosure, I'm not a Xamarin dev, so I'm not sure how those translate from Swift/Obj-C to Xamarin)

willDisplayCell of UTTableView is not called

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) {
}

Resources