UITableViewCell does not hold the state of custom button - ios

I have read through similar questions but I haven't been able to solve it. I have a tableview which has a custom cell with a button to add and remove a class. I created a custom delegate to save and remove the class, and to change the state of the button. Changing the state of the button works fine, but when I'm scrolling the buttons don't hold the state (I'm guessing is because I'm dequeuing the cells)
in my cell for row at index path i tried checking to change the state of the button:
let isInSchedule = self.isClassScheduled(classAttend, from: self.classesInSchedule!)
if isInSchedule == true {
cell.addRemoveButton.selected = true
} else {
cell.addRemoveButton.selected = false
}
and here is my delegate method where I save or remove the class
func indexOfClassSelectedWithButton(index: NSIndexPath, tableView:UITableView, and button: AddRemoveClass) {
if let currentlySavedClasses = ManagedObjectsController.sharedInstance.getAllScheduledClasses() as? [ClassScheduled] {
let classSelected = self.classes[index.section]
switch button.selected {
case true:
for classItem in currentlySavedClasses {
if classSelected.presentation?.title == classItem.presentation?.valueForKey("title") as? String {
ManagedObjectsController.sharedInstance.deleteScheduledClass(classItem)
button.selected = false
}
}
break
default:
if let classSelectedBreakout = classSelected.breakout?.valueForKey("breakoutID") as? String {
let canSave = self.isBreakoutAvailable(classSelectedBreakout, allClasses: currentlySavedClasses)
if canSave {
ManagedObjectsController.sharedInstance.createScheduledClass(from: classSelected)
button.selected = true
} else {
NSNotificationCenter.defaultCenter().postNotificationName(timeConlictNotication, object: nil)
}
}
break
}
}
}
it changes the button state but when I start scrolling up or down the buttons don't hold the state (I'm aware is probably because I'm revising my cell so it is taking any cell that it is available. Any help is greatly appreciated.
Also, I have my cell within my view controller in story board. Why is is that if i do cell = PresentationCell() all of the views in my cell are nil? Im just trying to not reuse the cell as my last solution.

Reusable table view cells do not keep the state so that to keep the button hold it's states you have to do an another check based on your condition when the cell will appear on the screen.
My suggestion is implement
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if isInSchedule == true {
cell.addRemoveButton.selected = true
} else {
cell.addRemoveButton.selected = false
}
}
Another thing is remembering reset states of cell before using by implement "prepareForReuse" in your custom cell.
override func prepareForReuse() {
}
I hope this would be help.

You should be doing two things
Override "prepareForReuse" in your UITableViewCell subclass to reset
the tableview cell to some default state
in tableView:cellForRowAtIndexPath: will dequeu a re-usable cell.
Since that cell is being re-used and may have already been
associated with a different indexPath you have to re-configure it.
In your case, you would set the state of the button to reflect the
indexPath you are about to assign the cell to

The solution came from #Michael " any state you need must be maintained outside of the cell" since the tableView reuses the cells that are already created I had to add a property to the class Im populating the tableview with (I used a bool but it can be anything) and based on that property I set the state of the button in the method cellForRowAtIndexPath.

Related

UIButton image for normal state in collectionview cell repeats itself every four cells

I'm trying to set an image for a button's normal state which is located in a collectionView cell. When the button is pressed the image changes. The problem is every four cells it repeats the same image as the original cell when the button is pressed. Is there a way to not have it repeat itself and when the button is pressed its only for that individual cell?
Here is the code:
class FavoritesCell: UICollectionViewCell {
var isFavorite: Bool = false
#IBOutlet weak var favoritesButton: UIButton!
#IBAction func favoritesButtonPressed(_ sender: UIButton) {
_ = self.isFavorite ? (self.isFavorite = false, self.favoritesButton.setImage(UIImage(named: "favUnselected"), for: .normal)) : (self.isFavorite = true, self.favoritesButton.setImage(UIImage(named: "favSelected"), for: .selected))
}
}
I've tried doing this but for some strange reason the 'selected' state image is never shown even when the button is pressed:
let button = UIButton()
override func awakeFromNib() {
super.awakeFromNib()
button.setImage(UIImage(named: "favUnselected"), for: .normal)
button.setImage(UIImage(named: "favSelected"), for: .selected)
}
Every time your cell is dequeued cellForItemAt is called. This is the place where you configure your cell data. So if you need to show cell marked as favourite, you can do it here.
So how do you do it there? Let's say all your cells are not selected in the beginning. Fine. You don't have to say anything in cellForItemAt. Now let's say you mark a few cells as favourite. What happens here is, it will reflect the change when the cell is visible because the button is hooked to a selector which will make the changes.
Now here is the problem. When you scroll and the cell disappears, the information about your cell being marked as favourite is lost! So what you need to do, is maintain an array which will store the IndexPath of all the selected cells. (Make sure to remove the IndexPath when a cell is removed from favourite!) Let's call that array favourites. If you can use your data source for the collection view to store the selected state information that is also fine. Now you have to store the information about whether your cell is marked as favourite in your button selector.
#objc func buttonTapped() {
if favourites.contains(indexPath) { // Assuming you store indexPath in cell or get it from superview
favourites.removeAll(where: {$0 == indexPath})
} else {
favourites.append(indexPath)
}
}
After you have stored the information about the cell, every time you dequeue a cell, you need to check if the IndexPath is favourites. If it is, you call a method which sets the cell in the selected state.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Dequeue cell and populating cell with data, etc
if favourites.contains(indexPath) {
cell.showFavourite()
}
}
Done? No! Now we have another problem. This problem is associated with the reuse of the cell. So what happens in cellForItemAt actually? You dequeue a cell and use it to display information. So when you dequeue it what happens is, it might have already been used for showing some other information in some other index path. So all the data that was existing there will persist. (Which is why you have the problem of favourites repeating every 4 cells!)
So how do we solve this? There is method in UICollectionViewCell which is called before a cell is dequeued - prepareCellForReuse. You need to implement this method in your cell and remove all the information from the cell, so that it is fresh when it arrives at cellForItemAt.
func prepareForReuse() {
//Remove label text, images, button selected state, etc
}
Or you could always set every value of everything inside the cell in cellForItemAt so that every information is always overwritten with the necessary value.
Edit: OP says he has a collection view inside a collection view. You can identify which collection view is called like this,
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView === favoriteCollectionView { // This is the collection view which contains the cell which needs to be marked as favourite
// Dequeue cell and populating cell with data, etc
if favourites.contains(indexPath) {
cell.showFavourite()
}
return cell
}
// Dequeue and return for the other collectionview
}
The cell is most likely reused and your isFavorite is set to true.
Just try adding
func prepareForReuse() {
super.prepareForReuse()
self.isFavorite = false
}
This will set the button to original image when cell is to be reused.
Also since you have your button have two states for selected why do this dance
_ = self.isFavorite ? (self.isFavorite = false, self.favoritesButton.setImage(UIImage(named: "favUnselected"), for: .normal)) : (self.isFavorite = true, self.favoritesButton.setImage(UIImage(named: "favSelected"), for: .selected))
where you could only say self.favoritesButton.selected = self.isFavorite
Change your cell code to:
class FavoritesCell: UICollectionViewCell {
#IBOutlet weak var favoritesButton: UIButton!
var isFavorite: Bool = false {
didSet {
favoritesButton.selected = isFavorite
}
}
#IBAction func favoritesButtonPressed(_ sender: UIButton) {
favoritesButton.selected = !favoritesButton.selected
}
override func prepareForReuse() {
super.prepareForReuse()
isFavorite = false
}
}

Showing and hiding a view only on a specific cell of a table view

I have a table view with custom cells. They are quite tall, so only one cell is completely visible on the screen and maybe, depending on the position of that cell, the top 25% of the second one. These cells represent dummy items, which have names. Inside of each cell there is a button. When tapped for the first time, it shows a small UIView inside the cell and adds the item to an array, and being tapped for the second time, hides it and removes the item. The part of adding and removing items works fine, however, there is a problem related to showing and hiding views because of the fact that cells are reused in a UITableView
When I add the view, for example, on the first cell, on the third or fourth cell (after the cell is reused) I can still see that view.
To prevent this I've tried to loop the array of items and check their names against each cell's name label's text. I know that this method is not very efficient (what if there are thousands of them?), but I've tried it anyway.
Here is the simple code for it (checkedItems is the array of items, for which the view should be visible):
if let cell = cell as? ItemTableViewCell {
if cell.itemNameLabel.text != nil {
for item in checkedItems {
if cell.itemNameLabel.text == item.name {
cell.checkedView.isHidden = false
} else {
cell.checkedView.isHidden = true
}
}
}
This code works fine at a first glance, but after digging a bit deeper some issues show up. When I tap on the first cell to show the view, and then I tap on the second one to show the view on it, too, it works fine. However, when I tap, for example, on the first one and the third one, the view on the first cell disappears, but the item is still in the array. I suspect, that the reason is still the fact of cells being reused because, again, cells are quite big in their height so the first cell is not visible when the third one is. I've tried to use the code above inside tableView(_:,cellForRow:) and tableView(_:,willDisplay:,forRowAt:) methods but the result is the same.
So, here is the problem: I need to find an EFFICIENT way to check cells and show the view ONLY inside of those which items are in the checkedItems array.
EDITED
Here is how the cell looks with and without the view (the purple circle is the button, and the view is the orange one)
And here is the code for the button:
protocol ItemTableViewCellDelegate: class {
func cellCheckButtonDidTapped(cell: ExampleTableViewCell)
}
Inside the cell:
#IBAction func checkButtonTapped(_ sender: UIButton) {
delegate?.cellCheckButtonDidTapped(cell: self)
}
Inside the view controller (NOTE: the code here just shows and hides the view. The purpose of the code is to show how the button interacts with the table view):
extension ItemCellsTableViewController: ItemTableViewCellDelegate {
func cellCheckButtonDidTapped(cell: ItemTableViewCell) {
UIView.transition(with: cell.checkedView, duration: 0.1, options: .transitionCrossDissolve, animations: {
cell.checkedView.isHidden = !cell.checkedView.isHidden
}, completion: nil)
}
EDITED 2
Here is the full code of tableView(_ cellForRowAt:) method (I've deleted the looping part from the question to make it clear what was the method initially doing). The item property on the cell just sets the name of the item (itemNameLabel's text).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
ItemTableViewCell.identifier, for: indexPath) as? ItemTableViewCell{
cell.item = items[indexPath.row]
cell.delegate = self
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
I've tried the solution, suggested here, but this doesn't work for me.
If you have faced with such a problem and know how to solve it, I would appreciate your help and suggestions very much.
Try this.
Define Globally : var arrIndexPaths = NSMutableArray()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = self.tblVW.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.textLabel?.text = String.init(format: "Row %d", indexPath.row)
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(btnTapped), for: .touchUpInside)
if arrIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.red.withAlphaComponent(0.2)
}
else {
cell.backgroundColor = UIColor.white
}
return cell;
}
#IBAction func btnTapped(_ sender: UIButton) {
let selectedIndexPath = NSIndexPath.init(row: sender.tag, section: 0)
// IF YOU WANT TO SHOW SINGLE SELECTED VIEW AT A TIME THAN TRY THIS
arrIndexPaths.removeAllObjects()
arrIndexPaths.add(selectedIndexPath)
self.tblVW.reloadData()
}
I would keep the state of your individual cells as part of the modeldata that lies behind every cell.
I assume that you have an array of model objects that you use when populating you tableview in tableView(_:,cellForRow:). That model is populated from some backend service that gives you some JSON, which you then map to model objects once the view is loaded the first time.
If you add a property to your model objects indicating whether the cell has been pressed or not, you can use that when you populate your cell.
You should probably create a "wrapper object" containing your original JSON data and then a variable containing the state, lets call it isHidden. You can either use a Bool value or you can use an enum if you're up for it. Here is an example using just a Bool
struct MyWrappedModel {
var yourJSONDataHere: YourModelType
var isHidden = true
init(yourJSONModel: YourModelType) {
self.yourJSONDataHere = yourJSONModel
}
}
In any case, when your cell is tapped (in didSelectRow) you would:
find the right MyWrappedModel object in your array of wrapped modeldata objects based on the indexpath
toggle the isHidden value on that
reload your affected row in the table view with reloadRows(at:with:)
In tableView(_:,cellForRow:) you can now check if isHidden and do some rendering based on that:
...//fetch the modelObject for the current IndexPath
cell.checkedView.isHidden = modelObject.isHidden
Futhermore, know that the method prepareForReuse exists on a UITableViewCell. This method is called when ever a cell is just about to be recycled. That means that you can use that as a last resort to "initialize" your table view cells before they are rendered. So in your case you could hide the checkedView as a default.
If you do this, you no longer have to use an array to keep track of which cells have been tapped. The modeldata it self knows what state it holds and is completely independent of cell positions and recycling.
Hope this helps.

last selected cell state is true after modifying tableview frame

Unexpected behaviour here.
I have a chat composed of a tableview and a bottom bar with a textView.
When the user select the textView the keyboard appears.
I use the notification UIKeyboardWillShowNotification to modify the frame of the tableview.
Selecting a row display a UIAlertController with different actions removing the keyboard.
This UIAlertController has a cancel action which is removing the selection of the cell (by selection i mean the grey style appearing when the selected state of the cell is true).
The selection is removed by setting cell.selected to false.
Problem is, when the keyboard appears, this previously selected cell is selected again, i tried to loop through the cells to put them all at selected false but depending on where the cell is located, the selected state will only appear after scrolling, making my loop uneffective.
Since i don't select the cells manually in my code, i assume this is a behaviour resulting maybe from the change of frame?
Is there something i can do to fix this?
Create an indexPath variable for referring to it later
var indexPath: IndexPath?
Inside tableView delegate didSelect Row assign the selected indexPath to the one you have created
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.indexPath = indexPath
}
Inside the function where you're listening for keyboard notification deselect the selected row using your referenced indexPath
func keyboardDidShow(notification: Notification) {
// your existing code...
if let indexPath = indexPath {
// deselect the selected row. I set animation to false
tableView.deselectRow(at: indexPath, animated: false)
// just a clean up set the indexPath to nil once done
self.indexPath = nil
}
}
This was a one way to fix the problem.
Each Cell has a selectedView you can set that Views backgroundColor to clearColor then the row will be selected but you won't see that. or you can return your own view for the selectedBackgroundView.
Adding a bit of code will help a lot.
With limited info, this is what I feel:
It may be due to reuse of a dequeued cell (since you make changes to the table when the keyboard notification is triggered).
This is my general practice:
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: CELL_REUSE_IDENTIFIER)
prepareDequeuedCellForReuse(cell)
The prepareDequeuedCellForReuse has something like this:
private func prepareDequeuedCellForReuse(_ dequedCell: UITableViewCell){
dequedCell.textLabel?.text = ""
dequedCell.tag = -1;
dequedCell.isSelected = false
}
I recommend you clear the selection here.

Extremely weird behavior with table cells when going off screen

Here is the really weird behavior. When the table view is first loaded, it looks like this:
Now, when I scroll down and then scroll back up, the buttons appear on cells that didn't have buttons on them before! Like so:
I know this has to do with "This is the intended behaviour of a UITableView. The whole point of a UITableView is to queue up cells that are needed and release cells that aren't needed to manage memory" as described in this post: UITableView in Swift: Offscreen cells are not pre-loaded.
Here is my code:
var messages = [String]()
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! CathyTaskLogTableViewCell
if messages[indexPath.row] != "" {
cell.messageButton.hidden = false
}
return cell
}
Anybody have a solution to this problem?
The reason for getting this result is because of UITableViewCell is being reuse.
if messages[indexPath.row] != "" {
cell.messageButton.hidden = false
}
else
{
cell.messageButton.hidden = true
}
There are two possible solutions to your problem:
Always set the hidden property:
cell.messageButton.hidden = messages[indexPath.row] != ""
Reset the state of your cell when it is reused, this provides deterministic behaviour in the table view controller, without burdening the controller class with tasks that a cell should do. This can be done by overwriting prepareForReuse in CathyTaskLogTableViewCell.
func prepareForReuse() {
super.prepareForReuse()
self.messageButton.hidden = true // or false, depending on what's the initial state
// other stuff that needs reset
}
With the current code, messageButton gets hidden only if the cell doesn't find something in messages. So for cells that had this button visible, were reused, and now correspond to a cell that doesn't have a correspondent in messages, the button will remain visible.

Stop the reuse of custom cells Swift

I have an uitableview with a custom cell which gets data from the array.
Custom cell has an uilabel and an uibutton (which is not visible until the uilabel text or the array object which loads for the text - is nil).
On launch everything is fine. When i press the uibutton the array is being appended, the new cells are being inserted below the cell.
But when i scroll - all of a sudden the uibutton appears on other cells where this conditional uilabel text isEmpty is not implied.
Here is how the whole process looks like
Here is my code for cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:TblCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! TblCell
cell.lblCarName.text = someTagsArray[indexPath.row]
if let text = cell.lblCarName.text where text.isEmpty {
cell.act1.hidden = false
} else {
println("Failed")
}
cell.act1.setTitle(answersdict[answersdict.endIndex - 2], forState:UIControlState.Normal)
cell.act2.setTitle(answersdict.last, forState:UIControlState.Normal)
return cell
}
So my general question is how do i stop the reuse of those custom cells?
As far as i'm aware there is no direct way of doing this on reusablecellswithidentifier in swift, but maybe there are some workarounds on that issue?
When a cell is reused, it still has the old values from its previous use.
You have to prepare it for reuse by resetting that flag which showed your hidden control.
You can do this either in tableView:cellForRowAtIndexPath: or the cell's prepareForReuse method.
Update:
Here's an example you can add for TblCell:
override func prepareForReuse()
{
super.prepareForReuse()
// Reset the cell for new row's data
self.act1.hidden = true
}

Resources