This question already has answers here:
Duplication in values of tableView when Scrolling Swift
(3 answers)
Closed 2 years ago.
I made a list where a user can see if an event is started, paused or stopped.
An event which is just created has no icon and no state and is returned with a created timestamp.
The problem which I have is that blank cells without an icon change their icon to "stop" when scrolling out of the view.
events.asObservable()
.bind(to:tableView.rx.items) { (tableView, row, event) in
let cell = tableView.dequeueReusableCell(withIdentifier: "EventCell") as! EventCell
cell.detailLabel.text = "created: \(self.dateFormat.string(from: event.created!)) Uhr"
cell.titleLabel.text = event.name
if let date = event.started {
cell.icon.image = UIImage(named: "start")
var dateStr = "started: \(self.dateFormat.string(from: date))"
if let paused = self.isPaused(event: event) {
if paused {
dateStr = "\(dateStr), paused"
cell.icon.image = UIImage(named: "pause")
}
}
if let dateEnded = event.ended {
dateStr = "\(dateStr), ended: \(self.dateFormat.string(from: dateEnded))"
cell.icon.image = UIImage(named: "stop")
}
}
return cell
}.disposed(by: disposeBag)
What is happening?
This occurs because of cell reuse. You should probably override the prepareForReuse method in EventCell and set the image to an initial image or nil before it's being reused at the next cellForRow (or cellForItem in UICollectionView) to fix this issue.
class EventCell: UITableViewCell { // (or UICollectionViewCell if issue is in UICollectionView)
//...
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil // or a default placeholder image.
}
}
Alternate Approach: You could also fix this issue in the cellForRow in UITableView (or cellForItem in UICollectionView) method by setting the imageView.image to nil or the placeholder image at the top and proceed.
For UITableView:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath)
cell.imageView.image = nil // or a default placeholder image.
//...
return cell
}
For UICollectionView:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath)
cell.imageView.image = nil // or a default placeholder image.
// ...
return cell
}
Related
I'm trying to show some images and hide others using ".isHidden" in my CollectionView. But when I scroll down or reload the collectionView they either get reordered incorrectly or hidden entirely.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ReadBookCell", for: indexPath) as! ReadBookCell
let item = readBookArray[indexPath.item]
for star in cell.starImgOutletCollection {
if star.tag <= item.starRating {
star.isHidden = false
} else {
star.isHidden = true
}
}
return cell
}
Edit: Here is my prepareForReuse
override func prepareForReuse() {
super.prepareForReuse()
for star in starImgOutletCollection {
star.isHidden = true
}
}
Assuming your "stars" are 5 image views in a stack view like this:
And, assuming your starRating will be between 0 and 5 (Zero being no rating yet)...
In your cell class, create a reference to the stack view - since your question mentions starImgOutletCollection I'm assuming you are using #IBOutlet (that is, not creating your views via code), so:
#IBOutlet var starsStackView: UIStackView!
Then, still in your cell class, add this func:
func updateStars(_ starRating: Int) {
for i in 0..<starsStackView.arrangedSubviews.count {
starsStackView.arrangedSubviews[i].isHidden = i >= starRating
}
}
Now, in cellForRowAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ReadBookCell", for: indexPath) as! ReadBookCell
let item = readBookArray[indexPath.item]
cell.updateStars(item.starRating)
// do the other stuff to set labels, images, etc
// in the cell
return cell
}
You no longer need the outlet collection for the "star" image views, and you no longer need to implement prepareForReuse().
I have purposely left an empty image in my assets catalog so that I can get my collectionView to somehow skip that image if it is nil, but so far it will render an empty image in that cell. It is better than crashing my app but how can I get it to skip that image?
here is my cellForItemAt indexPath code. Any ideas would be much appreciated.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: K.collectionViewCell, for: indexPath) as! CollectionViewCell
DispatchQueue.main.async {
let image = RoomModel.roomModel.rooms[indexPath.item]
if let image = image {
cell.imageView.image = image
}
}
return cell
}
Model Solution:
import UIKit
var data = [long list of UIImage(named: ...)]
class RoomModel {
var rooms = data.compactMap { ($0) }
var roomNo = 0
func setRoomNo(sender: Int) {
roomNo = sender
}
func getRoom() -> UIImage {
let image = rooms[roomNo]
return image
}
}
itemForRow Solution:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: K.collectionViewCell, for: indexPath) as! CollectionViewCell
DispatchQueue.main.async {
cell.imageView.image = self.roomModel.rooms[indexPath.item]
}
return cell
}
You would need to modify the data model to not include it if the image is nil and can be done by a simple if check before adding it in the model and then your cell would not render it as it is omitted from the data model.
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.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! LevelSelectCVC
cell.imageView?.image = imageArray[indexPath.row]
if indexPath.item >= stageProgress {
cell.imageView?.image = UIImage(named: "Lock Icon Grey")
} else {
cell.imageView?.image = imageArray[indexPath.item]
}
return cell
}
In this function I check the value of the integer stageProgress and add the same amount of images from an UIImage array to the collection view cells. The remaining of the cells adds a default UIImage. After returning from another view controller I have to check the stageProgress and add another UIImage from the array.
Does anyone know how I can do this? I have tried to do:
self.CollectionView.reloadData()
In both ViewDidAppear and PrepareForSegue
If I restart the game collectionView updates.
Calling ViewDidLoad() in ViewDidAppear resolved the issue.
I have used a tableview with 16 cells on a view controller. Each cell has a textfield and a picker view as a inputview for textfield. The odd thing is that When I choose the value for the first cell, it's fine. When I scrolled down to the last cell, the value is same as the first one. But I have never touched the last cell. Why would this happened?
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)
{
// selected value in Uipickerview in Swift
answerText.text = pickerDataSource[row]
answerText.tag = row
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = myTable.dequeueReusableCellWithIdentifier("addFollowCell", forIndexPath: indexPath) as! AddFollowTableViewCell
cell.questionView.text = listQuestion1[indexPath.row]
cell.pickerDataSource = dictPicker[indexPath.row]!
cell.answerText.addTarget(self, action: #selector(AddFollowUpViewController.textFieldDidChange(_:)), forControlEvents: UIControlEvents.EditingDidEnd)
return cell
}
func textFieldDidChange(sender: UITextField){
let rowIndex: Int!
let selectValue = sender.tag
if let txtf = sender as? UITextField {
if let superview = txtf.superview {
if let cell = superview.superview as? AddFollowTableViewCell {
rowIndex = myTable.indexPathForCell(cell)?.row
dictAnswer[rowIndex] = selectValue - 1
}
}
}
}
After two days, it solved by thousands of trials:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var cell = myTable.dequeueReusableCellWithIdentifier("addFollowCell") as! AddFollowTableViewCell
if(cell.identifier == true){
cell.answerText.text = selectedAnswerForRow[indexPath.row]
}
cell.questionView.text = listQuestion1[indexPath.row]
cell.pickerDataSource = dictPicker[indexPath.row]!
dictAnswer[indexPath.row] = cell.pickerValue
cell.answerText.addTarget(self, action: #selector(AddFollowUpViewController.textFieldDidChange(_:)), forControlEvents: UIControlEvents.EditingDidEnd)
cell.identifier = true
return cell
}
func textFieldDidChange(sender: UITextField){
let rowIndex: Int!
let cell = sender.superview?.superview as! AddFollowTableViewCell
rowIndex = myTable.indexPathForCell(cell)?.row
selectedAnswerForRow[rowIndex] = cell.answerValue
print(selectedAnswerForRow[rowIndex])
cell.answerText.text = sender.text
cell.identifier = true
}
It might have some performance issue need to be optimised , but it shows exactly what i want. LOL
You're basically recycling your views and not clearing them. That's the whole point of -dequeueReusableCellWithIdentifier:indexPath:.
Allocating and deallocating memory is very power consuming, so the system recycles every cell that goes out of viewport bounds.
You don't set the text inside answerText (I assume it's the text field that causes trouble) so its content will be kept when recycled.
Assuming you'll store user selection inside a dictionary var selectedAnswerForRow: [IndexPath:String]:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = myTable.dequeueReusableCellWithIdentifier("addFollowCell", forIndexPath: indexPath) as! AddFollowTableViewCell
cell.questionView.text = listQuestion1[indexPath.row]
cell.pickerDataSource = dictPicker[indexPath.row]!
cell.answerText.addTarget(self, action: "textFieldDidChange:"), forControlEvents: UIControlEvents.EditingDidEnd)
cell.answerText.text = self.selectedAnswerForRow[indexPath] ?? "" // add this
return cell
}
self.selectedAnswerForRow[indexPath] ?? "" returns the result or an empty string if it's not present in the dictionary.
Also, you're adding several times the action for edition control event. You have to check first if it isn't already bound.
Because the cell is reused. So you have to implement prepareForReuse() in your custom cell class and reset all the changing variables
UPDATE
See :
class MyCell : UITableViewCell {
#IBOutlet weak var myTextField : UITextField!
//Add the following
override func prepareForReuse() {
myTextField.text = nil
myTextField.inputView = myPickerView
super.prepareForReuse()
}
}