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?
Related
I have seen people writing this code inside a table view delegate
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! SuitCell? else {
fatalError()
}
...
}
now consider this other code
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! SuitCell
...
}
Won't both codes crash at the same lines if the cell is not dequeued?
Is there any difference? I am not seeing it.
dequeueReusableCell(withIdentifier:) can return nil in the case where there are no cells in the re-use pool (i.e. When the tableview is first shown). When it returns nil it is your responsibility to instantiate a cell of the appropriate type.
Therefore, this block of code:
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! SuitCell? else {
fatalError()
}
says "If you get a cell from the re-use pool and it isn't an instance of SuitCell, crash, but nil is OK" (Note the cast to an optional)
While this block of code:
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! SuitCell
Says "Crash if you don't get an instance of SuitCell, or youn get nil", so this will crash when the tableview is first shown.
dequeueReusableCell(withIdentifier:) isn't really used any more. You would use the newer (but still been around since iOS 6) dequeueReusableCell(withIdentifier:,for:) variant as it always returns a cell and you can expect it to be the right class (or you will quickly find your problem during development):
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) as! SuitCell
I have a table view that is initialized with three rows when loaded. Later, I want to extract cell data (from labels) and use it when that specific cell is selected by the user.
Even though initialization works well and I can see all the data being displayed, the dequeueReusableCell in the didSelectRowAt methods returns empty cells, with no data.
What is the problem?
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? ArticleTableViewCell else {
fatalError("Whoopse, dequeueing the cell failed")
}
let title = cell.articleLabel.text
// Do other stuff
}
The title variable above will be empty, even though it's data is shown on the display.
Replace
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? ArticleTableViewCell else {
fatalError("Whoopse, dequeueing the cell failed")
}
with ( not recommended )
guard let cell = tableView.cellForRow(at:indexPath) as? ArticleTableViewCell else {
fatalError("Whoopse, dequeueing the cell failed")
}
but ##better## is to access the data source array with the clicked index
let item = arr[indexPath.row]
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).
I have created an app to allow users to store various voice recordings against reviews. When I display this a table and the data is populated with the following code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = (indexPath as NSIndexPath).item
let cell = tableView.dequeueReusableCell(withIdentifier: "voiceRecordingCell", for: indexPath) as! VoiceRecordingTableViewCell
let voiceRecording = self.voiceRecordings[row] as! NSDictionary
let isoFormatter = DateFormatter()
isoFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'.000Z'"
let createdAt = isoFormatter.date(from: voiceRecording["created_at"] as! String)
self.recordingIndexPaths.insert(indexPath, at: row)
cell.recording = voiceRecording
cell.date.text = getDateFormatter("dd-MM-y").string(from: createdAt!)
cell.time.text = getDateFormatter("HH:mm").string(from: createdAt!)
cell.length.text = voiceRecording["length"] as? String
cell.location.text = voiceRecording["location"] as? String
let audioPlayerController = self.storyboard?.instantiateViewController(withIdentifier: "AudioPlayerController") as! AudioPlayerController
audioPlayerController.voiceRecording = voiceRecording
cell.audioPlayer.addSubview(audioPlayerController.view)
self.addChildViewController(audioPlayerController)
audioPlayerController.didMove(toParentViewController: self)
cell.deleteRecordingButton.tag = row
cell.deleteRecordingButton.addTarget(self, action: #selector(deleteRecordingPressed(_:)), for: .touchUpInside)
return cell
}
The cells all appear to be rendering correctly however for the cells that are not initially rendered with the page, the ones I have to scroll down to view, when I click on the buttons either on the audio player controls or the deleteRecordingButton nothing happens, its as though the addTarget is not being set. The code to set the buttons is being called and doesn't create an error, its just not applying to those button.
The buttons that are initially displayed on the screen have the correct actions and all work perfectly so I know that works.
I'm really at a loss as to what is causing this. I did try searching google and stackoverflow but I've not found anything. Any assistance with this would be greatly received.
--------------- UPDATE -----------
I just tried putting some breakpoints in
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
That also only works get called on the top 2 cells or the top one if in landscape!
Since the cell gets reused all the time the reference get lost.
Try something like this:
class ButtonTableViewCell: UITableViewCell {
typealias TapClosure = () -> Void
var buttonTapped: TapClosure?
#IBAction func buttonTouchUpInside(sender: UIButton) {
buttonTapped?()
}
}
extension TestViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! ButtonTableViewCell
cell.buttonTapped = {
print("Button tapped")
}
return cell
}
}
And another tipp. Never init an DateFormatter in cellForRowAtIndexPath. Instead create it in viewDidLoad or in a static struct for reuse.
Finally I have figured this out...
This was happening because I was using a scrollview with to scroll up and down the table without using the tables native scroll functionality.
When using the tables scroll functionality the buttons are applied the actions as they are brought into the view. This is handled by the func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { method. However when you do it the way I was it initially renders all the cells and the ones that are off the screen don't work!
Its a little bit annoying working in this way but at least I now know about it.
This question follows up from this: Use UICollectionViews to create dynamic and multiple features.
I am able to create a static cell which displays the name and image of the recipe similar like this app:
Where I am stuck is creating a dynamic row which changes based on the amount of data inside i.e. utensils or nutritional values like the image below:
I know how to display rows of data on tableView normally. But not sure how to embed it into a section inside a tableView. I attempted to add multiple prototype cells and assign them to a subclass of UITableViewCell's. Then I try to use if statements in my cellForRow but this isn't soling my issue.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! FirstCell
//set the data here
cell.recipeTitle.text = recipe.name
cell.imageView.image = UIImage(named: "url")
return cell
}
else if indexPath.row == 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell2", for: indexPath) as! SecondCell
//set the data here
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell3", for: indexPath) as! ThirdCell
//set the data here
return cell
}
}
I have also looked at this demonstration: https://medium.com/ios-os-x-development/ios-how-to-build-a-table-view-with-multiple-cell-types-2df91a206429, which is near to what I want to achieve but I have found it quite difficult to adapt to it.
If someone could direct me on how best to approach this or a good example then I would really appreciate it.
First you can't have static cells and dynamic cells in the same tableView. So how do you work around that? Define each of the static cells in the sections they belong in as well as the dynamic cells in the sections they belong to. That, however doesn't look like what you are trying to do. You just want multiple sections in the same tableView, each section with a different list of data
To do this you will need the number of sections so use the tableView(_:numberOfSections:) function.
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
You can then(and probably should) give each of those sections a title by initializing an array with the titles in your tableViewController(assuming thats what you are using. It could also just be a tableView).
let headerTitles = ["Nutritional Values", "Utensils", "Ingredients"]
Then use the tableView(_:titleForHeaderInSection:)
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section < headerTitles.count {
return headerTitles[section]
}
return nil
}
Now you can start defining your rows by the sections.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell
if indexPath.section == 0 {
//Setup up the first row
if indexPath.row == 0 {
//I'm not sure where or how you defined First/SecondCell but this may not work depending on those two questions.
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! FirstCell
return cell
} else if indexPath.row == 1 {
let cell = Bundle.main.loadNibNamed("StaticCell", owner: self, options: nil)?.first as! StaticCell
return cell
}
} else if indexPath.section == 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell1", for: indexPath) as! SecondCell
//setup cell1 n your storyboard to have a label with a tag # of 12(or whatever number you want to use)
//you also want an array with your utensil data accessible here
let label = cell.viewWithTag(12) as! UILabel
label.text = utensilNames[indexPath.row]
return cell
} else if indexPath.section == 2 {
let cellIngredients = tableView.dequeueReusableCell(withIdentifier: "Ingredients", for: indexPath)
tableView.deselectRow(at: indexPath, animated: true)
return cellIngreidents
}
cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
return cell
}
The point here is to use sections then rows to distribute your data.
Just to clarify Section 0 Row 0 - N would be where you're static rows are setup. I found it best to use XIB files subclassing TableViewCell.
Hope this helps.
EDIT So the way I'm looking at the "static" cells is in the first section the xib is the only put exactly where you tell it to be placed. In the example above the first section in the second cell is the