Swift - UITableViewCell with custom class and prototype repeating after 4th cell - ios

In my application, I have a UITableView which dynamically creates new cells as the user clicks on an "add" button. My cells have several fields that are intended to take user input. However, after creating a fourth cell, the cell contains duplicates of the input added in the first cell. For example, say each cell had a textfield
FirstCell.textfield.text = 0 <--- manually assigned
SecondCell.textfield.text = 1 <--- ..
ThirdCell.textfield.text = 2 <---- ..
FourthCell.textfield.text = 0 <--- automatically assigned
FifthCell.textfield.text = 1 <--- automatically assigned
After some digging, I believe this is due to the cells being dequeued using a reuse identifier and the cells being reused. How can I create multiple cells from the same prototype, but do not automatically hold the manually assigned values from the previous cell?
Update:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCustomCell", forIndexPath: indexPath) as! CustonUITableViewCellClass
cellB.delegate = self
return cell
}
I tried assigning each cell's UI element in this function according to indexPath.row, but it doesn't seem to be working. It'd be working fine until I start scrolling after adding 4 rows, the cell in the first row would return indexPath.row = 4 and all the UI elements in the first row would be assigned to the value inputted on the fourth row.

As you said, your cells are being reused and that's why you are experiencing this weird behavior. You need to hold on to the assigned values for each cell and clean/set each cell every time it is reused in cellForRowAtIndexPath.
Since you have a textField you could use the delegate to respond to it's text being changed and save that value in an array, then every time a cell is reused look for that value and set it.

Related

Cell Index Path while scrolling does not return intended results

I have a tableview that the user edits information in using textfields, and I store that information into an array that keeps track of all the values. The issue occurs when the user scrolls back to a cell they already edited and the values the added previously are now values from other cells.
I understand that cells are reused, and as a result their data needs to be updated whenever they are being viewed again. I also learned that cellforrowat is called every time a cell is loaded into the view as opposed to just the first time a cell is created. I made a test project to figure out my problem.
My first attempt at solving the problem went like so
cellforrowat is called
if this is the first time the cell is being made set default values and add its data to the array keeping our cell data
If this is not the first time, draw information from the data source at indexpath.row and apply it to the cell
if cellInformation.count < (indexPath.row + 1) // Newly made cell
{
cell.value = 0
cell.tField.text = ""
cellInformation[cellInformation.count] = cell
}
else if (cellInformation.count >= indexPath.row) // Cell we've seen before
{
cell.configure(Value: cellInformation[indexPath.row]!.value) // Sets the textField.text to be the same as the cells value
}
This worked better but when I scrolled back to the top of my tableview, the top most cells were still getting random data. My next attempt generated an ID tag for each cell, and then checking if the id tag of the cell at cellforrowat matched any of the one's in the array.
if cellInformation.count < (indexPath.row + 1) // 0 < 1
{
cell.idTag = idTagCounter
idTagCounter += 1
cell.value = 0
cell.tField.text = ""
cellInformation[cellInformation.count] = cell
}
else if (cellInformation.count >= indexPath.row)
{
for i in 0...idTagCounter - 1
{
if(cell.idTag == cellInformation[i]?.idTag)
{
cell.configure(Value: cellInformation[i]!.value)
}
}
cell.configure(Value: cellInformation[indexPath.row]!.value)
}
This got pretty much the same results as before. When I debugged my program, I realized that when i scroll down my tableview for the first time, indexPath.row jumps from a value like 7 down to 2 and as I scroll more and more, the row goes further away from what it should be for that cell until it eventually stops at 0 even if there are more cells i can scroll to. Once the row hits 0, cellforrowat stops being called.
Any ideas on how i can accurately assign a cells values to the information in my array?
Your premise is wrong:
cellforrowat is called
if this is the first time the cell is being made set default values and add its data to the array keeping our cell data
If this is not the first time, draw information from the data source at indexpath.row and apply it to the cell
You should set up a model object that contains the data for the entries in your table view, and your cellForRowAt() method should fetch the entry for the requested IndexPath.
Your model can be as simple as an array of structs, with one struct for each entry in your table. If you use a sectioned table view you might want an array of arrays (with the outer array containing sections, and the inner arrays containing the entries for each section.)
You should not be building your model (array) in calls to cellForRowAt().
You also should not, not NOT be storing cells into your model. You should store the data that you display in your cells (text strings, images, etc. Whatever is appropriate for your table view.)
Assume that cellForRowAt() can request cells in any order, and ask for the same cells more than once.
Say we want to display an array of animals, and a numeric age:
struct Animal {
let species: String
let age: Int
}
//Create an array to hold our model data, and populate it with sample data
var animals: [Animal] = [
Animal(species: "Lion", age: 3),
Animal(species: "Tiger", age: 7),
Animal(species: "Bear", age: 4)
]
//...
func cellForRow(at indexPath: IndexPath) -> UITableViewCell? {
let cell = dequeueReusableCell(withIdentifier: "cell" for: indexPath)
let thisAnimal = animals[indexPath.row]
cell.textLabel.text = "\(thisAnimal.species). Age = \(thisAnimal.species)"
}
Note that for modern code (iOS >=14), you should really be using UIListContentConfigurations to configure and build your cells.

Difference between tableView.cellForRow(at:) and tableView.dataSource?tableView(tableView:cellForRowAt:)

I'm unit testing on a tableView whether it renders a cell.
And I found that tableView.cellForRow(at:) returns nil, while tableView.dataSource?tableView(tableView:cellForRowAt:) returns the right cell.
Here's my unit test code.
it("renders one option text") {
let indexPath = IndexPath(row: 0, section: 0)
let cell = sut.tableView.dataSource?.tableView(sut.tableView, cellForRowAt: indexPath)
let cell2 = sut.tableView.cellForRow(at: indexPath)
expect(cell?.textLabel?.text).toEventually(equal("A1")) // test suceeded
expect(cell2?.textLabel?.text).toEventually(equal("A1")) // test failed
}
So I'm curious about the difference of the two methods.
Apple's document says that tableView.cellForRow(at:) returns nil if the cell is not visible, so I'v understood that tableView.cellForRow(at:) returns nil when it's under unit testing,
but I'm not sure the time order of the two methods being called and when tableView.cellForRow(at:) get the right value(cell).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
This method is used to generate or dequeue the cells as required by tableView. This is not the UITableView member method. Instead, it is a protocol method and another object, which will be a data source, will implement and return the value. So it will always return a cell whether we are unit testing or while debugging the app.
tableView.cellForRow(at:)
This method is not the generator method. It is a member method of UITableView as a utility method for eg. for getting selected row we use tableView.selectedRow. So it is supposed to return cell for any indexPath.
As we know UITableView doesn't create cells equal to rows drawn. Suppose you wanted to draw 100 rows then UITableView only create few extra cells apart from cells which are visible. So if you pass any indexPath which is not among the visible rows then practically that cell doesn't exist. Because tableview is waiting for you to scroll and reuse the unused cells. So whether you are doing unit testing or working on app it will always show nil for cells which are not visible.
tableView.dataSource?tableView(tableView:cellForRowAt:) will always dequeue a new cell. It isn't the one on display unless tableView is the one that called it.

Does indexpath.row take care of iterating an array?

I'm working through a UITableView tutorial and I've become curious about array iteration as I implement the UITableViewDataSource methods. When we call indexPath.row, is this calling a for loop for us behind the scenes? I'm asking because months back when I was learning to use data from a webservice (I've forgotten the exact steps of how I did it precisely) but I believe I needed to iterate over the array in order to present the information in the console.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create an instance of UITableViewCell, with default appearance
let cell = UITableViewCell.init(style: .value1, reuseIdentifier: "UITableViewCell")
// Set the text on the cell with the description of the item instance that is at the nth index of the allItems array, where n = row this cell will appear in on the tableview
let item = itemStore.allItems[indexPath.row]
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = "$\(item.valueInDollars)"
return cell
}
Am I correct in thinking that the indexPath.row method is iterating over the array for us behind the scenes?
let item = itemStore.allItems[indexPath.row]
No, calling indexPath.row does not iterate through all rows for you. It's just a variable sent to cellForRowAt that you can access and use to create your cell. It contains information about which row in which section the function cellForRowAt was called for.
However, cellForRowAt is actually called every time a cell is going to be visible on your tableVIew. Imagine you have a dataSource with 100 items and it is possible to only see 10 cells at a time. When the tableView gets initially loaded, cellForRowAt will get called 10 times. Then, if you scroll your tableView to show 3 more cells, cellForRowAt will get called 3 more times.
First of all the tutorial seems to be pretty bad. Table view cells are supposed to be reused
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
The workflow to display table view cells is:
The framework calls numberOfRowsInSection (and also numberOfSections)
For each section/row pair in the range of the visible cells it calls cellForRowAt and passes the index path in the second parameter.
No. The indexPath is just a struct with a section and a row. You use those to directly look up the information from your array. No iteration is happening.
cellForRowAt is only called for the cells that are on screen, so the order they are called depends on which direction you are scrolling.
In fact, the UITableView doesn't even know if you are using an array, or generating the information on the fly. What you do with the indexPath row and section is up to you.
Check the documentation of UITableView and cellForRowAtIndexpath functions.
An index path locating a row in tableView.
IndexPath will be a structure, which helps you to get the specific location from a nested array. In your case of the table view, rows inside the section.
cellForRow will return a cell for particular index path(in the specified section and row)
So indexPath.section will give the section number for which the cell is going to be modified and returned to the data source. And Inside the section, and indexPath.row will be the corresponding row inside that section.
index path documentation
tableView-CellForRowAt: IndexPath
UITableView DataSource Documentation
[update]
If you want to add multiple sections, add another dataSource numberOfSections, and return the number of sections count from it.
I can link to a create a table view tutorial from Apple, but it's a long document, you can skip the starting and read the To display a section in your table view.
If you want, you can use a 2D array for keeping sections and rows. And in numberOfSections method return array.count, and in numberOfRows:inSection, return array[section].count
More examples,
example
example
thanks,
J

iOS tableview cell is empty at random

Screenshot of weird behavior
The screenshot tells is quite well. I have a tableview with dynamic custom cells. I added a println for one of the contents of the cell to check, if the labels are set. I can see in the debug log, that each cell has its content. Still, on the device there are empty cells at random, which means, the row, where no content appears, is changing a lot. Even just scrolling up and down makes the second row disappear, but the third row is filled. Scrolling again turns this around again. If I close the app and start it again, every row is filled correctly.
Here is the code for the cell generation:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Return a count picker cell
if countPickerTableRow == indexPath.row {
...
}
// Return a normal wish list entry cell
else {
let article = wishListEntries[indexPath.row]!
let cell = tableView.dequeueReusableCellWithIdentifier("ArticleCell", forIndexPath: indexPath) as! WOSArticleCell
// Correct the order in case a count picker cell was inserted
var row = indexPath.row
if countPickerTableRow != -1 && indexPath.row > countPickerTableRow {
row--
}
cell.setThePreviewImage(UIImage(data: article.thumbnail))
cell.setArticleName(article.name)
cell.setArticleDescription(article.text)
cell.setArticleNumber(article.number)
cell.setArticleCount(article.count as Int)
cell.setOrderInTable(row)
cell.setTableViewController(self)
cell.setNeedsDisplay()
cell.setInputAccessoryView(numberToolbar) // do it for every relevant textfield if there are more than one
println(String(indexPath.row) + " " + cell.nameLabel.text!)
return cell
}
}
In the custom cell class there is nothing special. Just a few outlets to the labels.
Here is a screen of the storyboard:
Storyboard
Can anyone please help me finding out what is going on here? I can't find the reason why the debug log can output the contents of a cell, but the device is not able to render them.
You should change the logic of your code. If the PickerCell comes up just call reloadData() and reload everything in the tableview. If the amount of rows you have is small this won’t be an issue and it’s not an expensive operation as you are not doing any heavy calculating during display.
If you need to update only a single cell because of changes you made in the PickerCell then you should be calling reloadRowsAtIndexPaths:withRowAnimation: with the indexPath of the cell to be updated.
Your issue is with your subclass WOSArticleCell. Have you implemented prepareForUse()? If you have, are you setting any properties to nil?
UITableViewCell Class Reference
Discussion
If a UITableViewCell object is reusable—that is, it has a reuse
identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:. For
performance reasons, you should only reset attributes of the cell that
are not related to content, for example, alpha, editing, and selection
state. The table view's delegate in tableView:cellForRowAtIndexPath:
should always reset all content when reusing a cell. If the cell
object does not have an associated reuse identifier, this method is
not called. If you override this method, you must be sure to invoke
the superclass implementation.

Modifying cell of table view sorted by segmented control edited

I have a table view of tasks. I have also a segmented control above the table that has two choice, the first is for displaying the regular table the other one filters the array of the table to pick the elements that has the same date of the current date and then , I add these new elements to a new array and reload the table view that checks if the selected button in the segmented control is the second one , it will load the elements of filtered array as cells of the table. I have an action on the cell, so if you click them you go to a view where you can change the color of the cell. The problem is happening here. I store the index path of the cell in the table to determine which cell ( row ) is going to be edited, but if the selected segmented button is the second one, the index will be stored according to the order of the element in the filtered table , so when I finish editing the cell, the color will be changed of the cell that has that index not the actual cell because of the conflict between the normal table and the filtered table indexes. Hope that clear enough to explain my case , if it is not, ask me for more explanation.
here is the table loading code :
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
if sortTableClicked
{
return filteredAchivements.count
}else
{
return achievements.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("myCell2", forIndexPath: indexPath) as! AchievementsCells
var arr = [achievement]()
if sortTableClicked
{
arr = filteredAchivements
}else
{
arr = achievements
}
cell.name.text = arr[indexPath.row].name
cell.des.text = arr[indexPath.row].des
cell.date.text = "\(arr[indexPath.row].date) : تم إضافته بتاريخ "
cell.backgroundColor = arr[indexPath.row].cellColor
return cell
}
and I have this function that receives the index path sent from the table and apply the changes depending on this index path.
func applyEdit(color: UIColor, name: String, des: String, index: NSIndexPath) {
taskMgr.editCellDataAtIndex(index, forTable: "achievementsTable", theNewName: name, theNewDes: des, theNewColor: color)
taskMgr.loadData("achievementsTable")
achievements = []
achievements = taskMgr.achievements
tableView.reloadData()
}
what happens is that if the loaded table is the filtered one and let's say I want to edit the second cell , the indexpath.row will be 1 , so the changed- color cell will be the second one on the table , but actually the second cell in the filtered table is the fifth one in the non-filtered table , so I want to apply changes on this fifth one not the second one , does anyone have any idea ?
Problem is that you are saving cell index path which is static and is not changing with change in your table view.
You can fix this easily by adding one more bool property in your model arr that feeds data to table. Name is shouldChangeColor and set this property to true. Now, your model data is updated. Use this property in your cellForRowAtIndexPath function to change the cell colour.
Since cells are drawn in the order you supply your data from model, this would guarantee that your 2nd cell is painted in colour in filtered mode and 5th in other tab mode.

Resources