Swift TableView with multiple prototype cells are not displaying all Rows - swift

I have a UITableView created with 2 prototype cells, each of which have separate identifiers and subclasses.
My problem is when I display the cells the second prototype's first row gets absorbed under the first prototype cell.
For example, I have the first prototype cell displaying only 1 item. The second prototype cell should display 4 items. But, the first item from the second prototype cell is not displaying and, instead, there are only 3 of the four items visible.
Here is the code I have implemented:
override func viewDidLoad() {
super.viewDidLoad()
self.staticObjects.addObject("Please...")
self.objects.addObject("Help")
self.objects.addObject("Me")
self.objects.addObject("Thank")
self.objects.addObject("You")
self.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.objects.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.row==0){
let staticCell = self.tableView.dequeueReusableCellWithIdentifier("staticCell", forIndexPath: indexPath) as! StaticTableViewCell
staticCell.staticTitleLabel.text = self.staticObjects.objectAtIndex(indexPath.row) as? String
return staticCell
}
else{
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! PillarTableViewCell
cell.titleLabel.text = self.objects.objectAtIndex(indexPath.row) as? String
return cell
}
Thanks for all the help.

You have logic issues with how you are counting the number of rows in your table for both tableView:numberOfRowsInSection and tableView:cellForRowAtIndexPath.
Your code is producing a display output as shown below where:
The blue cells represent your staticCell prototype table cell view; these are the values from the staticsObjects array.
The yellow cells represent your cell prototype table cell view; these are the values from the objects array.
1. Look at tableView:cellForRowAtIndexPath:
In the cellForRowAtIndexPath method, you are only returning the count of the objects array.
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.objects.count
}
That means that the number of rows you will have in your table will be 4 instead of 5. Instead, you want to return the sum of the two arrays you are using in your table: objects.count + staticObjects.count. For example:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count + staticObjects.count
}
2. Look at tableView:cellForRowAtIndexPath:
Here's your original code with my comments..
// You are assuming that `staticObjects` will always have
// exactly one row. It's better practice to make this
// calculation more dynamic in case the array size changes.
if (indexPath.row==0){
let staticCell = self.tableView.dequeueReusableCellWithIdentifier("staticCell", forIndexPath: indexPath) as! StaticTableViewCell
staticCell.staticTitleLabel.text = self.staticObjects.objectAtIndex(indexPath.row) as? String
return staticCell
} else {
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! PillarTableViewCell
// Here's your problem! You need to calculate the row
// because you want to put the objects from your other
// array first. As a result, you need to account for them.
cell.titleLabel.text = self.objects.objectAtIndex(indexPath.row) as? String
return cell
}
Now, here's one way to fix your errors stated in the above discussion:
// If the indexPath.row is within the size range of staticObjects
// then display the cell as a "staticCell".
// Notice the use of "staticObjects.count" in the calculation.
if indexPath.row < staticObjects.count {
let staticCell = self.tableView.dequeueReusableCellWithIdentifier("staticCell", forIndexPath: indexPath) as! StaticTableViewCell
staticCell.staticTitleLabel.text = self.staticObjects.objectAtIndex(indexPath.row) as? String
return staticCell
} else {
// If you get here then you know that your indexPath.row is
// greater than the size of staticObjects. Now you want to
// display your objects values.
// You must calculate your row value. You CANNOT use the
// indexPath.row value because it does not directly translate
// to the objects array since you put the staticObjects ahead
// of them. As a result, subtract the size of the staticObjects
// from the indexPath.row.
let row = indexPath.row - staticObjects.count
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! PillarTableViewCell
cell.titleLabel.text = self.objects.objectAtIndex(row) as? String
return cell
}
Now you should see this:

Related

tableview swift uikit with firestore array

hello I have a problem creating a table view from firestore array of dictionary.
Note that the table view has the first cell that is a custom cell
for me the problem is because firestore array has only one dictionary as you could see here that is the the result of a print of the array plantDataArray
print("PLANT DATA ARRAY: \(plantDataArray)")
and I obtain this
PLANT DATA ARRAY: [Pietribiasi.PlantData(plantId: "C3884CIP01", plantType: "CIP", actualStatus: "WASHING", actualRecipe: "22")]
this is how I get the data from firestore and I put them on plantDataArray
func loadPlantData() {
db.collection("plantsData").whereField("plantId", isEqualTo: "C3884CIP01").getDocuments() { [self]
querySnapshot, error in
if let error = error {
print("Error: \(error.localizedDescription)")
}else{
self.plantDataArray = querySnapshot!.documents.compactMap({PlantData(dictionaryPlantData: $0.data()) })
DispatchQueue.main.async {
self.detailTableView.reloadData()
}
}
}
}
after I use this function
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return plantDataArray.count
}
and after this for generating the table view cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("INDEX ROW: \(indexPath.row)")
print("PLANT DATA ARRAY: \(plantDataArray)")
let customCell = detailTableView.dequeueReusableCell(withIdentifier: MyDetailTableViewCell.identifier, for: indexPath) as! MyDetailTableViewCell
customCell.selectionStyle = .none
let cell = detailTableView.dequeueReusableCell(withIdentifier: "DetailDefaultCell", for: indexPath)
switch indexPath.row {
case 0:
if plantDataArray[indexPath.row].plantType == "CIP"{
customCell.configure(imageName: "CIP")
} else if plantDataArray[indexPath.row].plantType == "PASTEURIZATION"{
customCell.configure(imageName: "PASTEURIZATION")
}
return customCell
case 1:
cell.textLabel?.text = "Actual Status:"
cell.detailTextLabel?.text = plantDataArray[indexPath.row].actualStatus
cell.detailTextLabel?.textColor = UIColor.gray
return cell
default:
return cell
}
}
but it generate only the first cell case 0 because plantDataArray.count is 1 so how I can solve this problem? It seams that I have to count the dictionary element and not the array of dictionary. Or what I'm doing wrong?
You want to have a row for each of the key value pairs in plantDataArray retrieved from Firestore. Currently, only "plantType" is shown instead of others. I think this is the intended behavior. Swift documentation for UITableView. Function tableView(UITableView, numberOfRowsInSection: Int) -> Int tells the data source to return the number of rows in a given section of a table view. In this case, you are returning "plantDataArray.count", which is 1 in this case. This means that the table view only contains 1 row, which makes sense why "switch indexPath.row" only returns case 0, as there is only 1 row in the view.
If you want to show all components in the same row, you will need to define the different properties in the tableViewCell. If you want the data to show in different rows, then you need to specify the exact number of rows, i.e. the number of keys in the plant data object.
For more information You can refer to the StackOverflow case1 and case2 where a similar issue related to population of tableview with data from firebase firestore has been discussed.

How to display a label on the last cell only?

I have a chat message table view with two cells to display, depending on whom sent the message.
I want the last cell to display the time, and only the last one. When I use tableView(:willDisplay cell:forRowAt indexPath:), the last cell doesn't show anything...
How can I display the time on that last cell?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if chatBubbles[indexPath.row].user == UserDefaultsService.shared.userID {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomCell.senderCellIdentifier.rawValue, for: indexPath) as! SenderTVC
populateSenderChatBubble(into: cell, at: indexPath)
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: CustomCell.conversationCellIdentifier.rawValue, for: indexPath) as! ConversationTVC
populateConversationChatBubble(into: cell, at: indexPath)
return cell
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == chatBubbles.count - 1 {
// What to do in here to display the last cell time?
}
}
Here is the method that display the cell content:
func populateSenderChatBubble(into cell: SenderTVC, at indexPath: IndexPath) {
let bubble = chatBubbles[indexPath.row]
let isoDateString = bubble.date
let trimmedIsoString = isoDateString.replacingOccurrences(of: StaticLabel.dateOccurence.rawValue, with: StaticLabel.emptyString.rawValue, options: .regularExpression)
let dateAndTime = ISO8601DateFormatter().date(from: trimmedIsoString)
date = dateAndTime!.asString(style: .short)
time = dateAndTime!.asString()
if dateAndTime!.isGreaterThanDate(dateToCompare: Date()) {
dateToShow = "\(date!) \(time!)"
}
else {
dateToShow = "\(time!)"
}
cell.senderDateLabel.text = dateToShow
cell.senderConversationLabel.text = bubble.content
}
The cell doesn't know it's last unless you tell it, but the tableView does know who's last. With that in mind, I would add a boolean in your cell like this:
var isLastCell: Bool = false {
didSet {
// do stuff if it's the last cell
if isLastCell {
// configure for isLastCell
} else {
// configure it for !isLastCell
}
}
}
When your custom UITableViewCell class initializes, it'll be with isLastCell = false, so assume that in your configuration. Whenever the boolean is updated to true, the cell will update via the didSet.
Then, in your cellForRow method, test the indexPath to see if it's the last indexPath of the datasource, if so, cell.isLastCell = true and the didSet in the cell will trigger to do whatever adjustments you need to do.
Another thing you'll need to do with this implementation is use cellForRow to update isLastCell for not just the last cell, but the cells that aren't last, since cells are created and destroyed all the time and the last cell at one moment might not be the last cell in another.

Thread 1: Fatal error: Index out of range from tableViewCell

I have error says "Thread 1: Fatal error: Index out of range".
on
cell.titleLabel.text = cellDataArrayPoster[indexPath.row].jobTitlePoster as? String
please notice that I'm using two different cells,
as prototypeCells. Moreover, they both have different identifier.
both arrays have getting their data from firebase.
var cellDataArray = [cellData]()
var cellDataArrayPoster = [cellDataPoster]()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellDataArray.count + cellDataArrayPoster.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let index = indexPath.row
if index == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! infoCell
cell.titleLabel.text = cellDataArray[indexPath.row].jobTitle as? String
cell.companyLabel.text = cellDataArray[indexPath.row].companyName
//cell.timeStampLabel.text = cellDataArray[indexPath.row].createdAt.calenderTimeSinceNow()
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellPoster", for: indexPath) as! infoCellPoster
cell.titleLabel.text = cellDataArrayPoster[indexPath.row].jobTitlePoster as? String
//cell.timeStampLabel.text = cellDataArray[indexPath.row].createdAt.calenderTimeSinceNow()
return cell
}
}
You misunderstood the concept of table view delegate methods. It is good to read more from the documentation.
My general rule of thumb is to always use only 1 array as data source for table view to avoid index out of range situations.
In your particular case the error is saying all about it - you are trying to reach index number that is out of range of the array. The easiest workaround will be to combine the two arrays in one, and have some sort of inheritance between the objects so they can fit.

How I can show only certain cells taken from Dictionary in a tableView in Swift

I am using a dictionary in order to fill a tableview.
Trying to appear only cells that have a certain userID, but it return also the cells that doesn't have this userID.
I have managed to count only the items from dictionary with the certain userID and if for example my dictionary has 8 entries and I need to show only the last 2 entries which have different userID, it returns 2 empty cells (which are the first 2 in the dictionary.
How I can get only the cells with the certain userID?
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
var returnCount:Int = 0
let currentUserId = NSUserDefaults.standardUserDefaults().stringForKey("userId")
for place in places {
if place["userID"] == currentUserId {
returnCount++
}
}
return returnCount
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let currentUserId = NSUserDefaults.standardUserDefaults().stringForKey("userId")
let currentPlacesUserId = places[indexPath.row]["userID"]
if currentPlacesUserId == currentUserId {
cell.textLabel!.text = places[indexPath.row]["name"]
cell.detailTextLabel?.text = places[indexPath.row]["issue"]
}
return cell
}
The fact is that you should not do this kind of logic inside de tableView delegate methods. Try getting the places from that userId when you load this array.
If you really want to proceed with the approach you are currently using try the following:
Not sure if this gonna work, but you are creating the cell even if it doesnt have the user Id you want. Try this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let currentUserId = NSUserDefaults.standardUserDefaults().stringForKey("userId")
let currentPlacesUserId = places[indexPath.row]["userID"]
if currentPlacesUserId == currentUserId {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel!.text = places[indexPath.row]["name"]
cell.detailTextLabel?.text = places[indexPath.row]["issue"]
return cell
} else{
return nil
}
}

How to show only filtered data in a table

How to show the data thats decks.status == true, and ignore those objects set to false?
data:
var decks: [DeckOfCards]
What I've got now:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as TableViewCell
if (thedeck.decks[indexPath.row].status == true) {
cell.label.text = "\(thedeck.decks[indexPath.row].card.name)"
}
}
You're going about this the wrong way. By the time you get to cellForRowAtIndexPath, you're already stated that a cell should be dequeued for this index path (and therefore at this index in your data array). The right place to be doing this filtering is in your data source.
For example, in addition to your decks array, you could make a computed property (filteredDecks) that gets its value by filtering the decks array.
var decks = [DeckOfCards]
var filteredDecks: [DeckOfCards] {
return decks.filter { $0.status }
}
You can then use this property as the data source for your table view.
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredDecks.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.label.text = "\(filteredDecks[indexPath.row].card.name)"
return cell
}
Now since this solution computes the filteredDecks array on each property access, it may not be the best approach if decks is a large array, or if you're reloading the table view frequently. If this is the case, and it's possible to do so, you should prefer filtering the decks array ahead of time using the same method shown in the computed property above.
You could use the filter function on the decks
let filteredDecks = decks.filter({$0.status})
Filter your array as
self.decks = self.decks.filter {
(d: DeckOfCards) -> Bool in
return d.status == true
}
Now your array will have the filtered values. you dont need to check for status inside cellForRowAtIndexPath function.