UITableView not dequeuing some cells - ios

I've never run into this issue before, but here is what I am currently experiencing:
I have a UITableView in a view controller. Delegate and dataSource are setup properly.
Now I'm running into performance issues after showing/inserting a few cells.
The issue is that the tableview seems to not dequeue some of the cells but rather creates a new one each time (always calls awakeFromNib in the custom class).
The ones with which the dequeuing seems to not work are just simple ones with a label and some with a label, an image and another label.
There is however one case in which dequeuing seems to work. I have a custom cell class that just contains a vertical stack view into which I dynamically add a variable amount of custom buttons.
I have a class that is responsible for setting up the cells. It has a method for each of the cell classes I use.
func initialModeratorCell(at indexPath: IndexPath, with message: Message, in tableView: UITableView) -> OnboardingInitialModeratorCell{
guard let cell = tableView.dequeueReusableCell(withIdentifier: initalModeratorCellIndentifier) as? OnboardingInitialModeratorCell else{
fatalError("No cell with \(initalModeratorCellIndentifier) identifier")
}
if indexPath.row != 0{
cell.moderatorImage.isHidden = true
}
let attributedMessage = attributedString(for: message, with: paragraphStyleFor(message: message))
cell.mesageText.attributedText = attributedMessage
cell.dateLabel.text = message.userType.name()
cell.mesageText.sizeToFit()
cell.dateLabel.sizeToFit()
return cell
}
All those methods look similar to this. This is one that doesn't seem to get reused.
This here is the one that does get reused (awakeFromNib only called once):
func componentCell(from item: ConversationItem, in tableView: UITableView, with owner: OnboardingComponentCellDelegate) -> OnboardingComponentCell{
guard let comp = item as? Component else{
fatalError("Type of item is not Component, but should be!")
}
guard let cell = tableView.dequeueReusableCell(withIdentifier: componentButtonsCellIdentifier) as? OnboardingComponentCell else{
fatalError("No cell with \(componentButtonsCellIdentifier) identifier")
}
cell.componentStack.translatesAutoresizingMaskIntoConstraints = false
setupComponentCell(cell, for: comp, owner: owner)
return cell;
}
This is setupCell():
setupComponentCell(_ cell: OnboardingComponentCell, for comp: Component, owner: OnboardingComponentCellDelegate){
cell.reset()
cell.component = comp
OnboardingComponentManager.createComponent(for: comp, in: cell, delegate: owner)
}
The cell.reset() method looks like this:
func reset(){
component = nil
delegate = nil
componentStack.removeAll() //removeAll is in an extension on UIStackView
}
The call OnboardingComponentManager.createComponent() just populates the stackview with the correct buttons for said component.
The methods above (componentCell(from:) and initialModeratorCell(at:) are called from the public method onboardingCellForItem(at indexPath: IndexPath, ..):
static func onboardingCellForItem(at indexPath: IndexPath, with displayedItems: Items, in tableView: Table, typingDelegate: IndicatorDelegate, buttonOwner: CellDelegate, onboardingType: ConvType) -> UITableViewCell{
let item = displayedItems[indexPath.row]
if item.type == .message{
return messageCell(from: item, at: indexPath, in: tableView, with: displayedItems)
}else if item.type == .component{
return componentCell(from: item, in: tableView, with: buttonOwner)
}else if item.type == .typingIndicator{
return typingIndicatorCell(with: typingDelegate, in: tableView)
}
return UITableViewCell()
}
This method then is called from the dataSource method cellForRowAtIndexPath like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return OnboardingCellManager.onboardingCellForItem(at: indexPath, with: displayedItems, in: tableView, typingDelegate: self, buttonOwner: self, onboardingType: .onboarding)
}
The cells were prototyped in a storyboard in the tableView (as prototype cells).
I'm somewhat hitting a wall here. I've never run into this issue before and I can't seem to find the reason as to why it happens.
This performance degradation is most notable on older devices (iPhone 5, etc).

Related

After tableView scrolled data puts in cells in wrong order

in my View:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TransactionTableCell", for: indexPath) as! TransactionTableCell
let newItem = getTransactionsInSection(section: sectionHeader[indexPath.section])[indexPath.row]
cell.configure(item: newItem)
}
in my TransactionTableCell
func configure(item: TransactionModel) {
guard let withdrawalBonuses = item.withdrawalBonuses,
withdrawalBonuses < 0,
let accruedBonuses = item.accruedBonuses,
accruedBonuses > 0 else {
configureWithOneOperation(item)//shows one line of operation
return
}
//show 2 lines of operations
firstOperationAmountLabel.text = "+\(Int(accruedBonuses))"
secondOperationAmountLabel.text = "\(Int(withdrawalBonuses))"
}
When I scroll the cell , second operation line is appears in wrong cells where its shouldn't be, even If I reload my table , that also has this problem.
You should use prepareForReuse() method
Simply just clear data of your labels:
override func prepareForReuse() {
super.prepareForReuse()
firstOperationAmountLabel.text = nil
secondOperationAmountLabel.text = nil
}
There are few things to check here.
Make sure you reset all fields before configure a new cell.
If you have created a cell using xib or storyboard, make sure you haven't filled labels with static text.
Is your guard statements passing for every item?
Else block for guard configures cell with a single operation, Is it handling all ui elements in cell?

Unable to dequeue a cell with identifier, but only when tapping search

I am having an issue with a UITableView Cell: "unable to dequeue a cell with identifier textCell", which I know is a common error, and I have read several of the entries here. The odd thing is, it works when I first draw the table. It does not fail until the user taps the search bar. I have checked the data at when the cell is being dequeued, and the various variables are populated, so the data is there.
I am using storyboards, so I should not need to register the cell, which is one of the common answers I have seen. Again, this is supported by the fact that the cell draw correctly at first. I was originally using dequeueReusableCell(withIdentifier:), but I also tried dequeueReusableCell(withIdentifier:for:), to no avail.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let ident = idList[indexPath.row]
var entryText = db.getText(ident)
let entryDate = db.getDateTimeLong(ident)
let comments = db.getComments(ident)
...
guard let cell = tableView.dequeueReusableCell(withIdentifier: "textCell", for: indexPath) as? TextTableViewCell else {
fatalError("NO SUCH CELL")
}
cell.dateLabel.text = entryDate
cell.textView.text = entryText
return cell
}
func updateSearchResults(for searchController: UISearchController) {
idList = db.textSearch(searchController.searchBar.text ?? "")
tableView.reloadData()
}
Any idea where I am going wrong?

Why two collection views in two table view cells won't work in Swift 4?

I read similar questions such as how to have multiple collection view in multiple table view cells and I connected my collection views cells and use identifier names for them but I don't know why I receive this Error:
* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'could not dequeue a view of kind: UICollectionElementKindCell with identifier extera_infoCollectionViewCell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'
* First throw call stack:
**Remember that I read Similar questions and the first table view cell with collection view working well and the problem is for second one **
here is my code for main view controller that has a table view and the table view has two cells
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView == fieldOfActivityCell().fieldofActivitiesCollectionView {
let fullfields : String = self.adv.resultValue[0].work_field!
let fullfieldsArr : [String] = fullfields.components(separatedBy: ",")
print(fullfieldsArr)
return fullfieldsArr.count
} else {
let extera_infofields : String = self.adv.resultValue[0].extera_info!
let extera_infofieldsArr : [String] = extera_infofields.components(separatedBy: ",")
print(extera_infofieldsArr)
return extera_infofieldsArr.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == fieldOfActivityCell().fieldofActivitiesCollectionView {
let fieldsCells = collectionView.dequeueReusableCell(withReuseIdentifier: "fieldOfActivityCollectionViewCell", for: indexPath) as! fieldOfActivityCollectionViewCell
let fullfields : String = self.adv.resultValue[0].work_field!
let fullfieldsArr : [String] = fullfields.components(separatedBy: ",")
fieldsCells.title.text = fullfieldsArr[indexPath.row]
return fieldsCells
}
else {
let extera_infoCells = collectionView.dequeueReusableCell(withReuseIdentifier: "extera_infoCollectionViewCell", for: indexPath) as! extera_infoCollectionViewCell
let extera_info : String = self.adv.resultValue[0].extera_info!
let extera_infoArr : [String] = extera_info.components(separatedBy: ",")
extera_infoCells.infoText.text = extera_infoArr[indexPath.row]
return extera_infoCells
}
}
and here is the table view codes in same view controller:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0{
let fieldCell = self.showAdvTableView.dequeueReusableCell(withIdentifier: "fieldOfActivityCell", for: indexPath) as! fieldOfActivityCell
return fieldCell
} else {
let fieldCell = self.showAdvTableView.dequeueReusableCell(withIdentifier: "extera_infoCell", for: indexPath) as! extera_infoCell
return fieldCell
}
here is table view first cell class:
class fieldOfActivityCell: UITableViewCell {
#IBOutlet weak var fieldofActivitiesCollectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
if let flowLayout = fieldofActivitiesCollectionView.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize.init(width: 1.0, height: 1.0) }
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
extension fieldOfActivityCell {
func setCollectionViewDataSourceDelegate
<D: UICollectionViewDelegate & UICollectionViewDataSource>
(_ dataSourceDelegate:D , forRow row : Int )
{
fieldofActivitiesCollectionView.delegate = dataSourceDelegate
fieldofActivitiesCollectionView.dataSource = dataSourceDelegate
fieldofActivitiesCollectionView.reloadData()
}
}
and here is the second tableview cell class:
#IBOutlet weak var extra_infoCollectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
if let flowLayout = extra_infoCollectionView.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize.init(width: 1.0, height: 1.0) }
}
}
extension extera_infoCell {
func setCollectionViewDataSourceDelegate
<D: UICollectionViewDelegate & UICollectionViewDataSource>
(_ dataSourceDelegate:D , forRow row : Int )
{
extra_infoCollectionView.delegate = dataSourceDelegate
extra_infoCollectionView.dataSource = dataSourceDelegate
extra_infoCollectionView.reloadData()
}
}
First step: using Tags - you just need to use tag for them and use if else to choose which collection view has selected with tag so the answer is this :
if collectionView.tag == 1 {
do some thing//////
}else {
do some thing else}
and you should use this in both cellForRowAtIndexPath and numberOfRows methods you can use this for table view too
Second step: you have to change the name of 'collection view' that you are dequeueing inside the cellForRowAt method in CollectionView data source:
if collectionView.tag == 1 {
let cell = yourFirstCollectionView.dequeueReusableCell(...) as yourCell
....
return cell
} else {
let cell = yourSecondCollectionView.dequeueReusableCell(...) as yourCell
....
return cell
}
According to your error your reuse identifier doesn't match any cell in your storyboard. Click on your extera_info collectionView cell in interface builder. Select the attributes inspector tab. Under reuse identifier make sure you put in extera_infoCollectionViewCell
If you take the other tableview cell In different class , with NSObject features of storyboard it can help you , And it is easy to maintain .
Saeed's tag option above is likely the simplest answer, but found his description a little short so adding a more complete answer below for those who've never used tags before...
If abiding by MVC and placing collectionView dataSource methods inside the UITableView class (instead of inside the UITableViewCell classes), and wanting to avoid this " error:
Each Collection View you use will need its own dequeueReusableCell identifier:
In interface-builder, name all your identifiers for your collection view cells. CatPicCell & DogPicCell for instance.
In your CellForItemAt collectionView method, set up if-statements or switch statement such that each reuse identifier is set equal to the identifiers you created in interface-builder (step 1). If using switch/case, your value can be set to collectionView.tag. Tags can be numbered to identify each different collectionView. The tags are like turning your set of collectionViews into a dictionary or array, such that each collectionView gets its own unique key/index.
Go back into interface-builder, and go into your storyboard and select each collection view (each of which should be inside its own tableView cell). In Xcode's "attribute inspector" scroll down to the "View" section and 3 spaces down (Xcode 11, Swift 5) you'll see a field called "Tag". Assign an integer value to that collection view, and then repeat this process for each collection view which is going to be embedded in your UITableView cells.
Once you have all the collection views tagged with unique integers, you simply set your cases to the integers, and give each dequeueReusableCell identifier the same integer index as you provided in the storyboard.
Now when you tableView cell calls on the collectionView you've outletted in the TableViewCell classes, it will be able to acquire the proper dequeueReusable ID. You can put your data inside each switch case.
Voila, you now have ONE collectionView datasource set of required methods, but serving ALL of your collection views. EVEN BETTER, when someone expands the project and adds another collectionView it will be as easy as adding another case to the switch and identifier in the storyboard.
Example code could look like this:
// I need a switch statement which will set the correct (of the 3 collectionViews) dequeueReusable IDENTIFIER for the collectionView
switch collectionView.tag {
//if tableView is doing cell == 1, then "CatsCell"
//if ... cell == 3, then "DogsCell"
//if ... cell == 5, then "BirdsCell"
case 1:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CatsCell", for: indexPath) as! CatsCVCell
// put your required data here
return cell
case 3:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DogCell", for: indexPath) as! DogsCVCell
// example data
let dogs = dogController.fetch()
cell.name = dogs[indexPath.item].dogName
if let image = UIImage(data: groups[indexPath.item].image!) {
cell.image = image
}
return cell
case 5:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BirdCell", for: indexPath) as! BirdCVCell
// put data code here for birds collection view cells
return cell
default:
return UICollectionViewCell() // or write a fatalError()
}
note: you have two options for your default to the switch statement...
1. like above, a generic but empty cell instance
2. throw an error. The error should never throw bc you'll have all the cases, but an error could occur if someone else improves your code and add another collectionView but forgets to to add the switch case-- so make your error statement explain what's wrong precisely.

Variable use of multiple custom cells

I'm using a unclickable tableView to display different information of one object.
For this informations I have different custom cell types one where I placed a map, if my object have locations, one have a list with links, and another a multiple line label for a little description...for example.
I manage this cells with:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! MapCell
return cell
} else if indexPath.row == 1 {
let cell: textCell = tableView.dequeueReusableCellWithIdentifier("textCell") as! TextCell
return cell
} else if indexPath.row == 2 {
let cell: listCell = tableView.dequeueReusableCellWithIdentifier("listCell") as! ListCell
return cell
}
}
So far so good, everything working fine. My problem is, not every object needs a map, some of them just need some text and a list, other objects need a map and a list, other all of them. I want my tableView to skip some cells if there is a condition.
I know, I can make an symbolic array for changing the number of cells of my tableView, but that deleting just from the end of my tableView, not specific cells.
One of my ideas is to generate a empty cell, maybe with a height of 0 or 1 so that I can do something like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
if mapCellNeeded {
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! mapCell
} else {
let cell: emptyCell = tableView.dequeueReusableCellWithIdentifier("emptyCell") as! EmptyCell
}
return cell
} else if indexPath.row == 1 {
...
}...
}
put I don't know if there isn't an efficient way. Hope you guys can help me.
Your solution would work. Another approach (very nice and swifty) would be not to hardcode row numbers, but rather use enum instead:
enum InfoCellType {
case Map
case Text
case Links
}
...
var rows = [InfoCellType]()
...
// when you know what should be there or not
func constructRows() {
if (mapCellNeeded) {
rows.append(InfoCellType.Map)
}
rows.append(InfoCellType.Text)
... etc
}
Then in the table view methods just see what's the type for current indexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellType: InfoCellType = self.rows[indexPath.row]
switch cellType {
case .Map:
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! mapCell
return cell
case .Text:
...
case.Links:
...
}
}
This solution also allows to easily change order of rows - just change the order of items in rows array.

reloadRowsAtIndexPaths doesn't update my cell data

I have a UITableView in my ViewController.
One of the cell could be tap into another TableViewController to allow select a value.
I want to update my cell after back from the callee ViewController.
right now, i could pass back the selected value by delegate.
However, i tried following way, none of them works.
self.mainTable.reloadData()
dispatch_async(dispatch_get_main_queue()) {
self.mainTable.reloadData()
}
self.mainTable.beginUpdates()
self.mainTable.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
self.mainTable.endUpdates()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
was called and executed without error.
but the UI just doesn't change
here is the way I update value in cellForRowAtIndexPath
if let currentCell = tableView.cellForRowAtIndexPath(indexPath) as UITableViewCell! {
currentCell.textLabel?.text = address
return currentCell
}
Here is my cellForRowAtIndexPath -
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let id = "Cell"
println(indexPath)
if indexPath.row == 1 {
var cell = tableView.dequeueReusableCellWithIdentifier(id) as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: id)
cell?.contentMode = UIViewContentMode.Center
cell?.selectionStyle = UITableViewCellSelectionStyle.None
cell?.contentView.addSubview(mapView!)
}
return cell!
}else{
let cell = UITableViewCell()
cell.textLabel?.text = self.address
return cell
}
}
Here is the delegate method -
func passBackSelectedAddress(address: String) {
self.address = address
var indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.mainTable.beginUpdates()
self.mainTable.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
self.mainTable.endUpdates()
}
My fix:
After more debug, i find the cause,
the self.address value is updated in delegate, however it roll back to previous value in cellForRowAtIndexPath.
I change the property to a static property, then resolve the problem.
I'm not sure what's wrong with instance property, and why it reverses back.
static var _address:String = ""
It seems like you're trying to grab a cell from the UITableView and then update the textLabel value that way. However, UITableView and UITableViewCell are not meant to be updated in this way. Instead, store the value of address in your class and update this value when the delegate calls back into your class. If cellForRowAtIndexPath constructs the UITableViewCell with the value of self.address, calling mainTable.reloadData() after should update the cell to the new value.
For example:
var address: String
func delegateCompleted(address: String) {
self.address = address
self.mainTable.reloadData()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(<your identifier>)
if (indexPath == <your address cell indexPath>) {
let textLabel = <get your textLabel from the cell>
textLabel?.text = self.address
}
return cell
}
Your cellForRowAtIndexPath has some problems -
You are using the same re-use identifier for different types of cell (one with a map, one without)
When you allocate the table view cell for the other row, you don't include the re-use identifier.
You have no way of referring to the map view that you are adding after the method exits because you don't keep a reference.
If you are using a storyboard then you should create the appropriate prototype cells and subclass(es) and assign the relevant cell reuse ids. If you aren't then I suggest you create a cell subclass and register the classes against the reuse identifiers. Your cellForRowAtIndexPath will then look something like -
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var returnCell:UITableViewCell
if indexPath.row == 1 {
var myMapCell = tableView.dequeueReusableCellWithIdentifier("mapCell", forIndexPath:indexPath) as MYMapCell
myMapCell.contentMode = UIViewContentMode.Center
myMapCell.selectionStyle = UITableViewCellSelectionStyle.None
// Set the properties for a map view in the cell rather than assigning adding an existing map view
returnCell=myMapCell
}else{
returnCell = tableView.dequeueReusableCellWithIdentifier("addressCell", forIndexPath:indexPath)
returnCell.textLabel?.text = self.address
}
return returnCell;
}

Resources