Crash with Missing cell for newly visible row when updating UITableView - ios

I am experiencing a crash when I delete a row.
// Updating my data model
....
// apply the updates
self.tableView.beginUpdates()
self.tableView.deleteRows(at: indexPathsToDelete, with: .automatic)
self.tableView.endUpdates()
Steps to reproduce
- Add rows
- Delete rows, specifically making sure there's some rows outside the current screen (that will then be in screen when the deletion is successful
- Repeat until crash occurs
It doesn't always happen so my best guess is that it will happen only when the cells it's trying to load get recycled
This is in 10.0 simulator with Xcode 8.0
*** Assertion failure in -[UITableView _updateWithItems:updateSupport:],
/BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3599.6/UITableView.m:3149
Missing cell for newly visible row 2
(null)
code for cellForRowAt
if isMultipe{
let cell = tableView.dequeueReusableCell(withIdentifier: DetailsTableViewCell.defaultIdentifier, for: indexPath) as! DetailsTableViewCell
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: DetailsMultipleTableViewCell.defaultIdentifier, for: indexPath) as! DetailsMultipleTableViewCell
return cell
}
the same bug reported here : https://forums.developer.apple.com/thread/49676

I had the same problem and I found out that UITableView has serious problems with animating insert/update/delete rows when section headers are variable height. Just convert the height to some constant and try it again.
This actually means deleting estimatedSectionHeaderHeight.

In my case it was crashing when cells have used autolayout for calculating it's height. So, the only guaranteed solution is use manual calculating heights. (what is actually sad..)

In my case I had this issue just for inserting the first row to an empty section, so I tried to fix it like this:
self.array.append(item)
if self.array.count > 1 {
self.tableView.beginUpdates()
self.tableView.insertRows(at: indexPathes, with: .top)
self.tableView.endUpdates()
} else {
self.tableView.reloadSections([1], with: .fade)
}

This problem happened to me when I had cells with different sizes. My code:
tableView.estimatedRowHeight = 150
tableView.rowHeight = UITableViewAutomaticDimension
During moving/inserting/deleting, app was crashing with "Missing cell for newly visible row" message. So I removed this line:
tableView.estimatedRowHeight = 150
and implement delegate method in which returned separate estimated height for each row.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {}
Hope it will help someone.

Remove indexPath in tableView.dequeueReusableCell
if isMultipe{
let cell = tableView.dequeueReusableCell(withIdentifier: DetailsTableViewCell.defaultIdentifier) as! DetailsTableViewCell
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: DetailsMultipleTableViewCell.defaultIdentifier) as! DetailsMultipleTableViewCell
return cell
}

I had a similar problem.
emrekyv suggested to delete estimatedSectionHeaderHeight but I have a table with one seaction and no header and no footer.
Expanding his idea and setting estimatedRowHeight to 0 (= not automatic) fixed the problem.
So the rule is:
Do not estimate any height when updating a UITableView.

In my case this happened when I forgot to call super.prepareForReuse() in my custom subclass.
See https://developer.apple.com/documentation/uikit/uitableviewcell/1623223-prepareforreuse:
If you override this method, you must be sure to invoke the superclass
implementation.

Related

Tableview cell setup using a ternery operator

I wonder if anyone can offer any guidance? I am writing an iPhone app, using Xcode 13.2.1. I am displaying a tableview within a scene that uses XIBs. It works fine. Above the table I have a header that is being displayed, it too works fine.
However, what I'd like to do is display the header, then display a cell that doesn't use the XIB (and that is a height of 50), and then displays every other cell after that first cell using a XIB (height is 195 - just an FYI). Thus, to do/implement this what I am trying to do is implement some kind of 'if statement' such that if indexPath.row is 0 then set the cell type to <call it cell type 1>, and if the indexPath.row is not 0 then set the cell type to <call it cell type 2>. I don't believe that I can use an IF statement because later in the code block it won't recognise the value of cell because it would have been set in an IF statement. Hence, I think I need to use a turnery operator, however I am struggling to construct the turnery operator.
The current code that sets up the cell for XIB in the
// MARK: TableView CELL Information
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Current code that sets up the cell for a XIB template
guard let cell: CustomTableViewCellTypeA = self.tableView.dequeueReusableCell(withIdentifier: "customCell") as? CustomTableViewCellTypeA else {
os_log("Dequeued cell isn't an instance of CustomTableViewCellTypeA", log: .default, type: .debug)
fatalError()
}
I KNOW THE FOLLOWING CODE DOESN'T WORK - however I am showing it this way to try and explain what I am trying to achieve:
// Intent is to use an IF or turnery operator to set the correct cell type
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
} else {
// Current code that sets up the cell for a XIB template
guard let cell: CustomTableViewCellTypeA = self.tableView.dequeueReusableCell(withIdentifier: "customCell") as? CustomTableViewCellTypeA else {
os_log("Dequeued cell isn't an instance of CustomTableViewCellTypeA", log: .default, type: .debug)
fatalError()
}
}
The follow on code (if I can somehow get the above to work) would mean that I would display some information in the first cell which would be <call it cell type 1> and then display other information in <call it cell type 2>.
Anyone done this before or would have any guidance on how to create such a turnery operator? I have tried many things but can't seem to manage to find the solution.
Cheers James.
Actually this was much easier to solve than trying to add complexity of turnery operators to determine which cell to dequeue. It was simply a case of using an IF and adding in the code I wanted to execute along with ensuring I put a return cell statement in it, meaning that if the IF-statement wasn't executed then the code executes the other dequeue statement... Thus, the code looks like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row < 1 {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Select/tap on an event record below to edit the details of the journey."
cell.textLabel?.textAlignment = .center
cell.textLabel?.numberOfLines = 0
cell.backgroundColor = .orange
return cell
}
guard let cell: CustomTableViewCellTypeA = self.tableView.dequeueReusableCell(withIdentifier: "customCell") as? CustomTableViewCellTypeA else {
os_log("Dequeued cell isn't an instance of CustomTableViewCellTypeA", log: .default, type: .debug)
fatalError()
}
This gave me the result I needed. Also, the feedback from Shawn Frank above helped me realise I hadn't registered the first cell type (only the second one), thus when I registered both the first and second cell types within the class it all worked beautifully. Thank you to all who looked at the question and the fine folks above who gave guidance. Cheers James.

Number of Lines for UILabel not Updating as Expected When TableViewCell is Tapped

I am changing the numberOfLines attribute on a label that lives in a custom UITableViewCell when the cell is tapped. However, this is not reflected in the UI until the second tap. The cell is configured as a prototype cell in the table view to initially have 2 lines.
Interestingly enough, when I print out the numberOfLines value before and after my tapped() function runs, the values start off different, and then synchronize - after the first tap, I see 2 lines before the function runs, then 0 lines after the function runs. However, after subsequent taps, I see the same value before and after my function, which makes it seem like it's not doing anything, even though the UI does stretch and shrink the cell, and the numberOfLines value is changed for the next time the didSelectRowAtIndexPath function runs.
I'm only seeing this behavior with tableView.reloadRows(). If I do a full update with tableView.reloadData(), the cell appropriately grows and collapses the first time it is tapped. However, this feels a bit ham-fisted and doesn't animate nicely like reloadRows() does.
TableView Implementation
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ReviewTableViewCell
else { return }
let data = tableData[indexPath.row]
print("old number of lines: \(cell.detailLabel.numberOfLines)")
//data.isOpen is set to false initially
cell.tapped(data.isOpen)
tableData[indexPath.row].isOpen = !data.isOpen
tableView.reloadRows(at: [indexPath], with: .fade)
print("old number of lines: \(cell.detailLabel.numberOfLines)")
// tableView.reloadData()
}
Custom Table View Cell method
func tapped(_ isOpen: Bool) {
if !isOpen {
detailLabel.numberOfLines = 0 }
else {
detailLabel.numberOfLines = 2 }
}
I am expecting this code to expand the cell once it is reloaded with tableView.reloadRows() if the numberOfLines is set to 0 and collapse the cell when it is set to 2. This does work, but only after tapping the cell two+ times. This should work with the first tap as well.
Here is a link of a gif that shows the issue: https://imgur.com/a/qe2uAXj
Here is a sample project that is similar to what's going on in my app: https://github.com/imattice/CellLabelExample
Just to be clear, to get this trick work UILabel generally must be constrained on each side to it's superview, in this way when it changes its intrinsicContentSize is able to push each side to accomodate the text.
Saying that, try to wrap the tapped method with those two methods:
tableView.beginUpdates()
if !isOpen {
detailLabel.numberOfLines = 0
}
else {
detailLabel.numberOfLines = 2
}
tableView.endUpdates()
Of course tableview must be set to automatic size:
tableView.estimatedRowHeight = <#What you want#>
tableView.rowHeight = UITableView.automaticDimension
I was able to work out what was going on. The problem is in two parts.
The first part is calling reloadRows(). This method is swapping out the cells with a new cell rather than updating the cell that already exists. Therefore, I'm changing the number of lines on that hidden swap cell rather than the cell that is in view. This behavior is mentioned in the docs:
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out.
Additionally, I'm using structs as the data model for tracking the open status of the cell. In Swift, structs are copy-on-write, which means that if a value is changed on that struct, a new struct is created rather than changing the value of that struct I'm pointing to. This means the line tableData[indexPath.row].isOpen = !data.isOpen doesn't do anything useful - we look at the tableData struct at the index path, get it's isOpen value, copy a new struct and change that new struct's isOpen value, and then throw it out because the new struct is not assigned anywhere.
The solution is to not use the reloadRows() method and to either use
A) a class for the data object
B) replace the data at indexPath.row to the copied struct
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CustomCell else { return }
var data = tableData[indexPath.row]
tableView.beginUpdates()
cell.tapped(isOpen: data.isOpen)
data.isOpen = !data.isOpen
tableData[indexPath.row] = data
tableView.endUpdates()
}

Swift MGSwipeTableCell Swipe to Delete Cell

I am using xCode 7 beta and Swift to implement a tableview with MGSwipeTableCells. I am doing this because I need to have a swipe button on both the left and right of each cell. Both of these buttons needs to remove the cell from the tableview.
I tried doing this by using the convenience callback method when adding the buttons to the cells:
// Layout table view cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("newsFeedCell", forIndexPath: indexPath) as! NewsFeedCell
cell.layoutIfNeeded()
// Add a remove button to the cell
let removeButton = MGSwipeButton(title: "Remove", backgroundColor: color.removeButtonColor, callback: {
(sender: MGSwipeTableCell!) -> Bool in
// FIXME: UPDATE model
self.numberOfEvents--
self.tableView.deleteSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
return true
})
cell.leftButtons = [removeButton]
However, once I delete the first cell, all the indices are thrown off and the callback now deletes an incorrect cell. That is, if I delete cell_0, cell_1 now becomes the first 0th in the table. However, the callback for the buttons associated with cell_1 delete the cell with index 1 even though it is actually now the 0th cell in the table.
I tried to implement the MGSwipeTableCell delegate methods, but to no avail. None of these methods were ever called in the execution of my code. How should I fix this problem? Will implementing the delegate solve this issue? If, so would it be possible to provide an example? If not, can you please suggest an alternate way to have tableview cells with swipe buttons on both sides that can delete said cells?
You can also do something like this to get the correct indexPath:
let removeButton = MGSwipeButton(title: "Remove", backgroundColor: color.removeButtonColor, callback: {
(sender: MGSwipeTableCell!) -> Bool in
let indexPath = self.tableView.indexPathForCell(sender)
self.tableView.deleteSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
return true
})
Using the delegate methods will allow for cleaner button creation and table cell removal, because the buttons will only be created for the cell when it is swiped (saves memory) and you can capture a weak reference to the 'sender' (an MGTableViewCell, or custom type) in the handler, from which you can then get the index path. Follow their example on Github:
MGSwipeTableCell/demo/MailAppDemo/MailAppDemo/MailViewController.m
Then in your cellForRowAtIndexPath, be sure to set the cell's delegate to 'self.' It looks like you're missing this, and it should fix your problem with delegate methods.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let reuseIdentifier = "cell"
let cell = self.table.dequeueReusableCellWithIdentifier(reuseIdentifier) as! MGSwipeTableCell!
cell.delegate = self
// Configure the cell
return cell
}
Happy coding!

UITableViewCells not displaying first time

I'm trying to create an autocompleter using iOS 8, Swift and Xcode 6.3
I have a problem that I'm trying to solve, but I gave up... I hope someone can help here. The problem is that (custom) UITableViewCell's are not displaying when the initial dataSource is empty. When adding data to datasource and reloading the tableView, the cells SHOULD display, but they don't... At least, the first time they don't... A second time, they DO... When I initialize the table with non-empty data, the problem doesn't occur. I guess something goes wrong with dequeueReusableCellWithIdentifier. In beginning, no reusable cells are found, or something. But I don't know why...
Relevant code, in ViewController.swift:
// filteredWords is a [String] with zero or more items
#IBAction func editingChanged(sender: UITextField) {
autocompleteTableView.hidden = sender.text.isEmpty
filteredWords = dataManager.getFilteredWords(sender.text)
refreshUI()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! AutocompleteTableViewCell
cell.title.text = filteredWords[indexPath.row]
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredWords.count
}
func refreshUI() {
self.autocompleteTableView.reloadData()
}
I created a sample project on github:
https://github.com/dirkpostma/swift-autocomplete
And a movie on YoutTube to show what goes wrong:
https://www.youtube.com/watch?v=ByMsy4AaHYI
Can anyone look at it and spot the bug...?
Thanks in advance!
You've accidentally hidden your cell.
Open Main.storyboard
Select Cell
Uncheck Hidden
Side note: As for why it's displaying the second time around with the cell hidden? It appears to be a bug. It should still be hidden (print cell.hidden, notice it's always true despite showing the text on the screen).
I think you need to change your code. Check out below code. It is because if you remember in Objective C you needed to check if the Cell was nil and then initialise it. The reuse identifier is usually reusing an already created cell, but on the first launch this does not work because there is no Cell to use. Your current code assumes always that the cell is created (re-used) because you are using ! in the declaration, so if you use the optional (?) it can be null and you then can create the cell
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as? AutocompleteTableViewCell
if cell == nil
{
//You should replace this with your initialisation of custom cell
cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "CELL")
}
cell.title.text = filteredWords[indexPath.row]
return cell

UITableView jumpy on scroll after changing cell height

So I have a tableView where I want to change the height of a cell when tapped. Well, actually, I am replacing it with a bigger cell.
On tap, I call:
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
tableView.endUpdate()
And then I modify my cellForRowAtIndexPath to return the right new cell and height. The cell's height is being automatically calculated by overriding sizeThatFits in the cell's implementation:
override func sizeThatFits(size: CGSize) -> CGSize {
return CGSizeMake(size.width, myHeight)
}
Oddly enough, after I do this, scrolling downwards is fine, but when I scroll upwards, the table jumps 5 or so pixels every second until I reach the top. After I reach the top of the table, the problem is gone and there is no jumping going in either direction. Any idea why this is happening? I imagine it has something to do with the new cell height displacing the other cells, but I can't see why the tableView is not taking care of this. Any help is appreciated!
Thanks!
EDIT: Added code from cellForRowAtIndexPath:
if self.openedCellIndex != nil && self.openedCellIndex == indexPath {
cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as ListCell
(cell as ListCell).updateWithDetailView(dayViewController!.view)
} else {
cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as ListCell
(cell as ListCell).updateWithData(eventDay: store.events![indexPath.row], reminderDay: store.reminders![indexPath.row])
}
return cell
You should include your code for cellForRow and heightForRow, but I will give it a blind shot.
When a cell is tapped in cellForRow you should store the index of that cell, then reload the data or just that cell. Then in heightForRow use if(yourTappedCell){return preferredHeight;}
Unfortunately, there does not seem to be a simple answer to this. I have struggled with it on multiple iOS apps.
The only solution I have found is to programmatically scroll to the top of your UITableView once it appears again.
[self.tableView setContentOffset:CGPointMake(0, 0 - self.tableView.contentInset.top) animated:YES];
OR
self.tableView.contentOffset = CGPointMake(0, 0 - self.tableView.contentInset.top);
Hope this an acceptable work around while still being able to use dynamic cell heights =)
func refreshCellState(indexPath:IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ListCell else {
return
}
if self.openedCellIndex != nil && self.openedCellIndex == indexPath {
cell.updateWithDetailView(dayViewController!.view)
} else {
cell.updateWithData(eventDay: store.events![indexPath.row], reminderDay: store.reminders![indexPath.row])
}
self.tableView.beginUpdates()
self.tableView.endUpdates()
if #available(iOS 11.0, *) {
self.tableView.performBatchUpdates {
} completion: { complete in
if complete {
self.tableView.scrollToRow(at: indexPath, at: .none, animated: true)
}
}
} else {
// Fallback on earlier versions
self.tableView.reloadData()
}
}
Here, Dont reload that cell entirely, But take that cell when clicked and provide data to cell from there.
Now call begin and endUpdated methods of tableview to update height.

Resources