cellForRowAt (UITableViewDelegate) called and noticing indexing is wrong (Swift) - ios

From Google Firestore, I am fetching modeled user data individually. Once 1 user's data has been completely fetched, a completion handler is executed and the user will be added to a global array of users (self.activeUsers) and then I call reloadData() on my UITableView (self.activeUsersTableView). I noticed users are getting loaded on top of each other and it is not a UI construction issue or auto layout issues. I can tell because when I print out the indexPath.row in cellForRowAt, I notice duplicate prints of the same index. Anybody got an idea as to why? reloadData() is ONLY called in 1 place for this UITableView which is in the body of myCompletionHandler
private lazy var activeUsersTableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
table.dataSource = self
table.delegate = self
table.allowsSelection = false
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
loadActiveUsersTableView()
fetchActiveUsers()
}
func loadActiveUsersTableView() {
self.view.addSubview(activeUsersTableView)
self.activeUsersTableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[
self.activeUsersTableView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 150),
self.activeUsersTableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 30),
self.activeUsersTableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -280),
self.activeUsersTableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -250),
]
)
}
func fetchActiveUsers() {
let myCompletionHandler: (UserProfile) -> Void = { theUser in
self.activeUsers.append(theUser)
self.activeUsersTableView.reloadData()
}
self.chatsDocRef!.getDocument { document, error in
if let error = error as NSError? {
print("Error getting document: \(error.localizedDescription)")
}
else {
if let document = document {
let data = document.data()
self.activeUserUIDS = data?["users"] as? [String]
for i in 0..<(self.activeUserUIDS?.count ?? 0) {
print("Retrieving document for user ID: ", self.activeUserUIDS![i])
let userDocRef = Firestore.firestore().collection("users").document(self.activeUserUIDS![i])
UserProfile.getUserProfileData(userDocRef, using: myCompletionHandler)
}
}
}
}
}
Here is the cellForRowAt call and followed by that is my output from the print method. I left out the code AFTER the print for better readability (it isn't important in this case)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("The row", indexPath.row)
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
return cell
}
OUTPUT: The row 0
The row 0
The row 1
The row 0
The row 1
The row 2
The row 0
The row 1
The row 2
The row 3
The row 0
The row 1
The row 2
The row 3
The row 4
The row 0
The row 1
The row 2
The row 3
The row 4
The row 5
The row 0
The row 1
The row 2
The row 3
The row 4
The row 5
The row 0
The row 1
The row 2
The row 3
The row 4
The row 5
The row 0
The row 1
The row 2
The row 3
The row 4
The row 5

the real root of the problem was the fact I was not using custom cell design and clearing my cells prior to reuse by using prepareForReuse() method. I implemented a custom cell design and this fixed my issue.

Related

iOS: Invalid update: invalid number of rows in section n

I am building an iOS app, basically the user create an item by pressing the "+" button and then the app should put the new item in according section of the table based on the location of the item. However I got the error: Invalid update: invalid number of rows in section 1. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update. Here is my code, thank you
let sections = ["Bathroom", "Bedroom", "Dining Room", "Garage", "Kitchen", "Living Room"]
#IBAction func addNewItem(_ sender: UIBarButtonItem) {
/*
//Make a new index path for the 0th section, last row
let lastRow = tableView.numberOfRows(inSection: 0)
let indexPath = IndexPath(row: lastRow, section: 0)
//insert this new row into the table
tableView.insertRows(at: [indexPath], with: .automatic)*/
//Create a new item and add it to the store
let newItem = itemStore.createItem()
var Num: Int = 0
if let index = itemStore.allItems.index(of: newItem) {
switch newItem.room {
case "Bathroom":
Num = 0
print("I am in here")
case "Bedroom":
Num = 1
case "Dining Room":
Num = 2
case "Garage":
Num = 3
case "Kitchen":
Num = 4
case "Living Room":
Num = 5
default:
Num = 0
print("I am in Default")
}
let indexPath = IndexPath(row: index, section: Num)
tableView.insertRows(at: [indexPath], with: .automatic)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemStore.allItems.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//create an instance of UITableViewCell, with default appearance
//let cell = UITableViewCell(style: .value, reuseIdentifier: "UITableViewCell")
//get a new or recycled cell
//if indexPath.row < itemStore.allItems.count {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
//Set the text on the cell with the decription of the item
//that is at the nth index of items, where n = row this cell
//will appear in on the table view
let item = itemStore.allItems[indexPath.row]
if item.name == "" {
cell.nameLabel.text = "Name"
} else {
cell.nameLabel.text = item.name
}
if item.serialNumber == nil {
cell.serialNumberLabel.text = "Serial Number"
} else {
cell.serialNumberLabel.text = item.serialNumber
}
cell.valueLabel.text = "$\(item.valueInDollars)"
cell.roomLabel.text = item.room
if item.valueInDollars < 50 {
cell.valueLabel.textColor = UIColor.green
}else if item.valueInDollars >= 50 {
cell.valueLabel.textColor = UIColor.red
}
cell.backgroundColor = UIColor.clear
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let LocationName = sections[section]
return LocationName
}
thank you so much for your time!
and this is how I create item
#discardableResult func createItem() -> Item {
let newItem = Item(random: false)
allItems.append(newItem)
return newItem
}
The problem is that you should insert the item in the array first:
itemStore.allItems.append(newItem)
Also, there is a difference between sections and rows in numberOfRowsInSection(return number of rows for every section) you have a switch that returns the same number, it should be
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemStore.allItems.count
}
Edit :
The problem is when the table loads there is 0 rows for all the sections ( itemStore.allItems.count is zero ), when you try to insert a row say at section 0 , row 0 -- the dataSource must be updated only for that section , which is not happen in your case as it's the same array that is returned for all sections , so you must either have an array of array where inner array represent number of rows so addition/deletion from it doesn't affect other ones ,,,,, or lock the insert to say section 0 like this
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (section == 0 ) {
return itemStore.allItems.count
}
return 0
}
in this edit i inserted in 2 sections 0 and 2 with no crash because i handled numberOfRowsInSection to return old numbers for old section that why to be able to insert in all sections you must have a different data source array or manage from numberOfRowsInSection , see edited demo here Homepwner
Instead of setting footer in viewDidLoad implement this method
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
if(section == 5 ) {
let textLabel = UILabel()
textLabel.text = "No more Item"
return textLabel
}
return nil
}
There is something terribly wrong with your data source. Your numberOfRow for all sections is the same. i.e. itemStore.allItems.count. Which means you are setting the same number of rows in each section.
The main issue is, while adding a new item, you are inserting a new row in the specific section which means
new number of rows for section = previous number of rows in section +
1
However, there is no addition in the data source.
So according to the code, you have inserted a new row, but your data source has one record less since you didn't add the item there.
So I would like you do the following in order of these steps :
Add item in required data source.
inserRowInSection
Update : Remove the condition in cellForRowAtIndexPath :
if indexPath.section <= 1 and it's else block. You don't need that. If number of sections is less than 1, it won't be called. Moreover it's the else block which is getting called all the time because you already have sections in your code. So if case will never be called.
So I got the issue. In itemStore.createItem() the value for room is always Room. In the switch-case, you never have Room case. So it always falls in default case. You need to fix that.
#Jacky.S I downloads your code and try to debug it.Fist look our error properly.
reason: 'Invalid update: invalid number of rows in section 1. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (0)
So It says before update, your sections has 0 rows. Now you try to update you tableview using this piece of code.
print(indexPath)
tableView.insertRows(at: [indexPath], with: .automatic)
I just added the print statement for debugging purpose and it prints [0, 0] .So you say your tableview to insert 0th row on section 0.
Now when you insert any row into tableview it calls the delegate method.Now look at the below delegate method.
override func tableView(_ tableView: UITableView, numberOfRowsInSection
section: Int) -> Int {
if section == 0{
print("0",itemStore.allItems.count)
}
if section == 1{
print("1",itemStore.allItems.count)
}
// print(itemStore.allItems)
return itemStore.allItems.count
}
In your code you just return itemStore.allItems.count.I added the above code for debugging.Now look that itemStore.allItems.count always return 1.So for first section it works perfectly because few times ago you insert a row in first section whose index is [0 = row, 0 = section].So you previously returned rows for section 0 is equal to the recently return rows for section 0.But When it comes for section 1 you previously return no row that means your section 1 has no rows in previous but in your above delegate method you also return 1 row for section 1.Which is conflicting because in previous you never update or return any rows for section 1.That's why you code crash and and say that
your number of rows contained in an existing section after the update is 1 which must be equal to the number of rows contained in that section before the update. Which is 0.
So this is the explanation for you crash.
Now, to recover from crash your allItems should be a dictionary or an array of array.
var allItems = [[Item]]()
or
var allItems = ["String":[Item]]()
guess allItems element is something like this
allItems = ["bathroom":[item1, item2, item3],"bedroom":[item1, item2, item3, item4]]
so here you have 2 section one is bathroom and another is bedroom.First section have 3 row and second one have 4 row and in you tableview delegate method you have to return 3 for section 0 and 4 for section 1

CollectionView cell inside UITableView is repeating

I have one table view in which every cell contains one collection view!
1) display only two cell in collectionview as my issue is when we scroll the tableview, collectoinview cell content is repeated in many other cell of tableview
myCode:
let tags = Tblarr[indexPath.row]["hashtags"] as! [String]
if tags.count != 0
{
for var i in 0 ... tags.count - 1
{
if i < 2
{
cell.tagsView.addTag(tags[i])
cell.collectionView.isHidden = false
}
}
if tags.count > 2
{
cell.lblCount.isHidden = false
cell.lblCount.text = "+\(tags.count - 2)"
}
else
{
cell.lblCount.isHidden = true
}
}
else{
cell.lblCount.isHidden = true
cell.collectionView.isHidden = true
}
cell.fillCollectionView(with: cell.tagsView.tags)
cell.widthConstraint.constant = cell.collectionView.contentSize.width
here I get only fixed width of collectionview without setting width according to content size & collectionview cells are repeated in other cell of tableview, even if cell is one , it will show 2 cell(when scrolled)
when load for the first time
when tableview is scrolled
//for storing array in collectionview from tableview
func fillCollectionView(with array: [String]) {
self.collectionArr = array
self.collectionView.reloadData()
}
reload collectionView inside tableview
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
cell.fillCollectionView.reloadData()
}
func fillCollectionView(with array: [String]) {
self.collectionArr.removeAll
self.collectionArr = array
self.collectionView.reloadData()
}
I don't know what fillCollectionView does but it looks like you didn't consider that the table view cells are reused. So every time you call fillCollectionView you have to reset the content of your cell or the content or your collection view.

Reloading Table View to Create Dynamic Label Changes iOS Swift

I am creating an app that I want to display different strings inside of a label that is within a UITableViewCell. However, I cannot use indexPath.row because the strings are in not particular order.
let one = "This is the first quote"
let two = "This is the second quote"
var numberOfRows = 1
var quoteNumber = 0
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let items = [one, two]
myTableView = tableView
let cell = tableView.dequeueReusableCellWithIdentifier("customcell", forIndexPath: indexPath) as! cellTableViewCell
let cellWidth = cell.frame.width
let cellHeight = cell.frame.height
cell.QuoteLabel.frame = CGRectMake(cellWidth/8, cellHeight/4, cellWidth/1.3, cellHeight/2)
let item = items[quoteNumber]
cell.QuoteLabel.text = item
cell.QuoteLabel.textColor = UIColor.blackColor()
if quoteNumber == 0 {
quoteNumber = 1
}
if numberOfRows == 1 {
numberOfRows = 2
}
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
var timer = NSTimer.scheduledTimerWithTimeInterval(5, target: self, selector: "update", userInfo: nil, repeats: true)
}
func update() {
dispatch_async(dispatch_get_main_queue()) {
self.myTableView.reloadData()
}
This code builds and runs, but when the timer I have set expires and the update function is ran, both of the two available cells change to the same label. What I am trying to do is make it so that the first cell can remain static while the newly added cell can receive the new element from the 'items' array without using indexpath.row (because the quotes that I am going to be using from the items array will be in no particular order). Any help would be appreciated.
Instead of reloadData try using reloadRowsAtIndexPaths: and pass in the rows you want to reload. This does use indexPath, however. The only way I'm aware of to reload specific rows as opposed to the whole tableView is to specify the rows by index path.

self.tableView.indexPathForCell(cell) returning wrong cell?

I have a UITableView (multiple sections) with custom dynamic cells. Each cell has a UIStepper representing selected quantity.
In order to send the selected quantity (UIStepper.value) from the cell back to my UITableViewController, I have implemented the following protocol:
protocol UITableViewCellUpdateDelegate {
func cellDidChangeValue(cell: MenuItemTableViewCell)
}
And this is the IBAction in my custom cell where the UIStepper is hooked:
#IBAction func PressStepper(sender: UIStepper) {
quantity = Int(cellQuantityStepper.value)
cellQuantity.text = "\(quantity)"
self.delegate?.cellDidChangeValue(self)
}
And from within my UITableViewController I capture it via:
func cellDidChangeValue(cell: MenuItemTableViewCell) {
guard let indexPath = self.tableView.indexPathForCell(cell) else {
return
}
// Update data source - we have cell and its indexPath
let itemsPerSection = items.filter({ $0.category == self.categories[indexPath.section] })
let item = itemsPerSection[indexPath.row]
// get quantity from cell
item.quantity = cell.quantity
}
The above setup works well, for most cases. I can't figure out how to solve the following problem. Here's an example to illustrate:
I set a UIStepper.value of 3 for cell 1 - section 1.
I scroll down to the bottom of the table view so that cell 1 - section 1 is well out of view.
I set a UIStepper.value of 5 for cell 1 - section 4.
I scroll back up to the top of so that cell 1 - section 1 is back into view.
I increase UIStepper by 1. So quantity should have been 4. Instead, it's 6.
Debugging the whole thing shows that this line (in the delegate implementation of UITableViewController) gets the wrong quantity. It seems indexPathForCell is getting a wrong cell thus returning a wrong quantity?
// cell.quantity is wrong
item.quantity = cell.quantity
For completeness sake, here's cellForRowAtIndexPath implementation where the cells are being dequeued as they come into view:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "MenuItemTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! MenuItemTableViewCell
cell.delegate = self
// Match category (section) with items from data source
let itemsPerSection = items.filter({ $0.category == self.categories[indexPath.section] })
let item = itemsPerSection[indexPath.row]
// cell data
cell.cellTitle.text = item.name + " " + formatter.stringFromNumber(item.price)!
cell.cellDescription.text = item.description
cell.cellPrice.text = String(item.price)
if item.setZeroQuantity == true {
item.quantity = 0
cell.cellQuantityStepper.value = 0
cell.cellQuantity.text = String(item.quantity)
// reset for next time
item.setZeroQuantity = false
}
else {
cell.cellQuantity.text = String(item.quantity)
}
.....
....
..
.
}
You need to update the stepper value in the else clause in cellForRowAtIndexPath:
else {
cell.cellQuantity.text = String(item.quantity)
cell.cellQuantityStepper.value = Double(item.quantity)
}
Otherwise the stepper retains the value from the previous time the cell was used.

weird behavior on UItableView that is not inside UITableViewController

I have a UIViewController, and when a user clicks a button, I want to show a UITableView. I receive the data from the server.
The problem is that there is something weird happening. Sometimes, not all the cells are updated. In other words, in my prototype cell, there are two buttons, with default text, but when I load the data from server not all the buttons text are updated, instead when I scroll the table, they become updated. Also when I **scroll* the table, sometimes the buttons go back to the default text.
Here are my code: (really easy)
class CusinePreferencesTableView: NSObject, UITableViewDelegate, UITableViewDataSource {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentefiers.oneCusinePreferencesCell.rawValue, forIndexPath: indexPath) as! OneCusinePreferencesTableViewCell
var row = indexPath.row
print("row = \(row)")
let oneCusineDataLeft = Preferences2ViewController.cusines![row]
cell.leftButton.titleLabel?.text = oneCusineDataLeft
row = row + 1
if row < Preferences2ViewController.cusines!.count{
let oneCusineDataRight = Preferences2ViewController.cusines![row]
cell.rightButton.titleLabel?.text = oneCusineDataRight
}else {
//I should hide the right button
}
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let cusines = Preferences2ViewController.cusines {
if cusines.count % 2 == 0 {
return cusines.count/2
}else {
var count = cusines.count/2
count = count + 1
return count
}
}else {
return 0
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
}
And this is the table view (in case you want it)
If you see that you didn't get the description of my problem, I can make a video for you
Update 1
I already call the reloadData as you see here (where I get the data from the server)
func loadCusiens(){
let url = NSURL(string: ConstantData.getWebserviceFullAddress()+"preferences/cusines")
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "POST"
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {(data, response, error ) in
if let error = error {
print("error = \(error)")
}
if let data = data {
do{
let jsonArray = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! NSArray
var results = [String]()
for oneJSON in jsonArray {
let name = oneJSON["name"] as! String
results.append(name)
}
dispatch_async(dispatch_get_main_queue(), {
Preferences2ViewController.cusines = results
self.cusineTableView.reloadData()
})
} catch{
print("This exception happened = \(error)")
}
}
})
task.resume()
}
Update
Now in the else part, i set the text of the right button to "sometext"
this is a video about the problem
http://www.mediafire.com/watch/k2113ovebdvj46d/IMG_0911.MOV
Update
The main problem is not cell reuse, nor the layout problem I explain below, but the use of:
cell.leftButton.titleLabel?.text = oneCusineDataLeft
to set the button text. You must use:
cell.leftButton.setTitle(oneCusineDataLeft, forState: .Normal)
instead. (In essence, a UIButton keeps track of the title text it should display when it is in different "states" - normal, selected, etc. Although the your code changes the text that is currently displayed, as soon as the button changes state, it sets the text back to the stored value. I assume the button state is reset when the cells are displayed. The setTitle method updates the stored value).
Original
Setting aside the cell reuse problem, I don't think your code will achieve quite what you want. As currently coded, the layout would be something like this:
Row 0: left: cuisine 0, right: cuisine 1
Row 1: left: cuisine 1, right: cuisine 2
Row 2: left: cuisine 2, right: cuisine 3
I'm guessing you actually want this:
Row 0: left: cuisine 0, right: cuisine 1
Row 1: left: cuisine 2, right: cuisine 3
Row 2: left: cuisine 4, right: cuisine 5
If that's the case, amend your code as follows:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentefiers.oneCusinePreferencesCell.rawValue, forIndexPath: indexPath) as! OneCusinePreferencesTableViewCell
var row = indexPath.row
print("row = \(row)")
let oneCusineDataLeft = Preferences2ViewController.cusines![2*row]
cell.leftButton.titleLabel?.text = oneCusineDataLeft
if (2*row+1) < Preferences2ViewController.cusines!.count{
let oneCusineDataRight = Preferences2ViewController.cusines![2*row+1]
cell.rightButton.titleLabel?.text = oneCusineDataRight
}else {
//I should hide the right button
cell.rightButton.titleLabel?.text = ""
}
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let cusines = Preferences2ViewController.cusines {
if cusines.count % 2 == 0 {
return cusines.count/2
}else {
return (cusines.count+1)/2
}
}else {
return 0
}
}
I should also note that you would probably be better off using a UICollectionView rather than a UITableView, since the former will provide the multiple column flow much more easily and intuitively.
Looks like a reuse problem. Basically if you follow the examples in Apple and other places, it uses a dequeue function. What this means is that iOS will look for an already existing cell that is no longer being shown and give you that to draw the contents of a new cell. But of course if the old cell had a bunch of stuff set, you will see the old stuff. The solution is to always assign everything to the correct new value.
This can get you easily if your cells have variable information, for instance if there's a picture you set if a certain entry has it, but isn't set if it doesn't have it. What will happen then is when you get an entry with no picture your code might not set it the image to nil, and you'll see the picture of an old cell that was scrolled off.
Please let me explain the reuse of UITableViewCells works
Assume you have a very huge cells and you can see only 3 cells on you screen.
Assume now you see on your screen the following cells:
A
B
C
On the screen... and your datasource is: A B C C B A.
Now you are scrolling down for 1 cell exactly:
So the cell A is going from the Up (it was first cell) down and becoming your last visible cell
So now on the screen you will see
B
C
A // While you want to draw a cell `C` here according to the dataSource
What happens now in cellForRowAtIndexPath...
You are getting the same Cell A from the beginning.
Assume your cell A had only 1 button and a Cell C need to show 2 buttons
Because you have already draw the cell A you draw it with 1 button (lets assume you set the right button hidden property to YES).
So now you want to draw your cell C on the reused cell A
(Assume you set TAGS for the cell types to know what type of cell is popped up to you in the cellForRowAtIndexPath)
And now you can query the tag of the cell
if(myCell.tag == `A`/*(Assume you have an Enum for tags)*/)
{
//So here you will want to Unhide the Right button of the cell (which is currently still cell A
//After you have done with all needed adjustments you will want to set the appropriate tag for this cell to be ready for the next reuse -> so set it's tag to C
//Now next time this cell will get reused it will be drawn as cell `C` and it will think it is a cell `C`
}

Resources