Selection of UITableViewCell Changes when Scroll down in Swift - ios

I am using a UITableView and what I am doing is I am changing the color of the cell when I tap on the cell using didSelectRow function of UITableView at cellForRowAt. The thing which is bothering me is when I scroll down or scroll up, those cells whom I changed the color before were changed to other cells. Here is my code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = myTableView.dequeueReusableCell(withIdentifier: "TasksTableViewCell") as! TasksTableViewCell
cell.backView.backgroundColor = .white
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = myTableView.cellForRow(at: indexPath) as! TasksTableViewCell
cell.backView.backgroundColor = UIColor(named: "primaryViewColor")
}
Does anyone knows why this happens? Does anyone has a solution that when only those cells changes color whom I tap on, and when I scroll down or move up only those cells have the other color?

cellForRowAt will be called every time that cell is displayed.
you need selected list to save selected index.
var listSelected: [Int] = []
and
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TasksTableViewCell") as! TasksTableViewCell
cell.backView.backgroundColor = listSelected.contains(indexPath.row) ? UIColor(named: "primaryViewColor") : .white
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if listSelected.contains(indexPath.row) {
listSelected = listSelected.filter{$0 != indexPath.row}
} else {
listSelected.append(indexPath.row)
}
tableView.reloadRows(at: [indexPath], with: .automatic)
}

I encountered do you see the problem many times. Even if using and iVar can solve the problem, You are mixing "Controller" logic and "Model" logic.
I usually prefer to move "selection" state inside the model.
Suppose You have a class "Contact" you use to fill cell data (usual MVC pattern)
I add:
class contact{
..
var selected = false
}
AND in TV delegation method I use to apply selection, OR better I use a custom selection method in a custom cell (for example to see a √ element in cell)
As a bonus multiple selection come for free, and you can also save current selections for next run :)

So as I understand you select a cell and after that other cells look like they are selected?
If so I think this is happening because you change the background color of the cell and tableViews and collectionViews are reusing the cells, basically keeping the background you changed behind.

TableViewCells are reused as soon as they leave the visible area.
This means that a cell whose background you have colored will be deleted from the view hierarchy as soon as it is scrolled up or down. If the corresponding row is scrolled in again, the function cellForRowAt is called again for this IndexPath and the cell gets a white background.
The easiest is to save the IndexPaths of the selected cells and check in the cellForRowAt function if the current cell has to be selected.
Add the following var to the viewController class:
var selectedIndexPaths = Set<IndexPath>()
and modify the tableView delegate methods:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = myTableView.dequeueReusableCell(withIdentifier: "TasksTableViewCell") as! TasksTableViewCell
cell.backView.backgroundColor = (selectedIndexPaths.contains(indexPath) ? UIColor(named: "primaryViewColor") : .white)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
if selectedIndexPaths.contains(indexPath)
{
selectedIndexPaths.remove(indexPath)
}
else
{
selectedIndexPaths.insert(indexPath)
}
tableView.reloadRows(at: [indexPath], with: .none)
}

You can use
step 1: create model
class DemoModel {
var isSelected: Bool = false
var color: UIColor = .While
}
step 2: and in tableview
var listDemo: [DemoModel] = [DemoModel(),...]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = myTableView.dequeueReusableCell(withIdentifier:
"TasksTableViewCell") as! TasksTableViewCell
var obj = listDemo[indexPath.row]
cell.backView.backgroundColor = obj.color
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var obj = listDemo[indexPath.row]
obj.color = UIColor(named: "primaryViewColor")
tableView.reloadRows(at: [indexPath], with: .automatic)
}

Related

how to perform specific action at certain table view row in swift?

I have a table view cell. I make an app for a tenant in the apartment to report the defect of the room facility. if the defect has been repaired (status: Complete), data from server will give defect.status == 2 (if defect.status == 1, still on process to be repaired), and it will show YES and NO Button like the picture above.
I want if it still on the repairment process, the view that contains "Are You satisfied" label and Yes No Button will not appear. The expected result should be like the picture below
here is the code I use to remove that satisfied or not view
extension RequestDefectVC : UITableViewDataSource {
//MARK: Table View Delegate & Datasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listDefects.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.removeFromSuperview()
}
}
return cell
}
}
but unfortunately, if that wantToRemoveView.removeFromSuperview() is triggered, it will remove all the view in all cell, even though the status is completed like picture below
I want that satisfied or not view appears if the status is complete, otherwise, it will be removed. how to do that ?
For your costumed cells are reused, removing views will cause uncertain effects. You don't actually need the specific view to be removed, only if it stays invisible.
if dataDefect.status == 2 {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.isHidden = true
}
} else {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.isHidden = false
}
}
Create a height constraint for that view and hook it as IBOutlet and control it's constant according to that in cellForRowAt
self.askViewH.constant = show ? 50 : 0
cell.layoutIfNeeded()
return cell
I expect you using automatic tableView cells
#Alexa289 One suggestion is that you can take heightConstraints of UIView. then create IBOutlet of your height constraints and make its constant 0 when you want to hide otherwise assign value to your static height.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
cell.viewSatisficationHeightConstraints.constant = 50
} else {
cell.viewSatisficationHeightConstraints.constant = 0
}
return cell
}
Second suggestion is that you can take label and button in view and embed stackview to your view(view contain label and button)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
cell.viewSatisfication.isHidden = false
} else {
cell.viewSatisfication.isHidden = true
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 40
}
you can read about UIStackView which makes hiding things easier. If you are not using stackview and hiding things the UI will not good as the space used by the hidden view will be still there. So better to use stackView when need to hide or show some view.
UIStackView : https://developer.apple.com/documentation/uikit/uistackview

Table view did select method to handle check box image

I have an table view with three cell which contains the label and one image (check box).Now when ever i select any cell.That particular cell image (check box) alone needs to get tick.png. And remaining two cell image should be untick.png.
But now if i select first cell then the first cel image get as tick.png.Then if i select second and third cell.That cell image also getting tick.png
But i need only one image alone needs to tick.png.Which ever table view cell i am selecting that particular cell image alone needs to be tick.png.And remaining two cell image should be untick.png.
My code :
var Data: [String] = ["First","Second","Third"]
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.Data.count > 0{
return self.Data.count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ViewCell
cell.Lbl.text = self.aData[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! suggestionCell
cell.suggestionImg.image = UIImage(named: "tick")
}
If I understand you correctly you only want a single check mark at any given time. If this is true then you would simply setup a property in your view controller like this:
var checkedRow: Int
and set the row index in tableView(_:didSelectRowAt:). By setting it to -1 you would disable all check marks. Then in tableView(_:, cellForRowAt:) you would conditionally enable the check mark for the cell if indexPath.row is equal to checkedRow:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
checkedRow = indexPath.row
tableView.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ViewCell
if indexPath.row == checkedRow {
cell.suggestionImg.image = UIImage(named: "tick.png")
cell. suggestionLbl.text = "<ticked text>"
} else {
cell.suggestionImg.image = UIImage(named "untick.png")
cell. suggestionLbl.text = "<unticked text>"
}
return cell
}
As an add-on to Tom's answer, I suggest storing IndexPath instead of Int adding also a
var lastCheckedRow:IndexPath = IndexPath(row: 0, section: 0)
This allows you to only reload the newly checked row and the previously checked row instead of the whole table view plus it will support multiple sections too. It does not matter much at your current stage where there is only 3 rows but for larger table views this will be more efficient. Also it removes the blinking effect of UITableView.reloadData().
The code is something like:
//0 based on assumption that first cell is checked by default
var checkedRow:IndexPath = IndexPath(row: 0, section: 0)
var lastCheckedRow:IndexPath = IndexPath(row: 0, section: 0)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//Update checkedRow for reload but keep track of current tick
lastCheckedRow = checkedRow
checkedRow = indexPath
//Remove previous tick
tableView.reloadRows(at: [lastCheckedRow], with: .automatic)
//Update new tick
tableView.reloadRows(at: [checkedRow], with: .automatic)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ViewCell
if indexPath.row == checkedRow {
cell.suggestionImg.image = UIImage(named: "tick.png")
} else {
cell.suggestionImg.image = UIImage(named "untick.png")
}
return cell
}
You can also play around to create an ideal visual effect when ticking different cell by changing the with:UITableViewRowAnimation parameter which I use .automatic for the example.
allowsMultipleSelection: Is only easiest thing that will help you.
Add following line in your viewDidLoad after setting up the tableView
override func viewDidLoad() {
// ... setup you need
tableView.allowsMultipleSelection = false
}
Hope this helps!

Why in heightForRowAt method my cell is nil?

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let cell = tableView.cellForRow(at: indexPath) as? ExpandablePlaneTableViewCell {
return 400.0
}
return 75.0
}
I want to change size of my cell but inside of heightForRowAt it cannot see my cell and crashed. When I put there if let check it does not enter inside of the block and just takes 75.
Can anyone tell me what the problem is? It's too strange for me!
I already set delegate to self. So it call the function but cannot detect my cell there.
UPDATE
In my ExpandablePlaneTableViewCell I have a variable:
var exapanded = false
Later in my ViewController: On click on the button in the cell I run my delegate method:
func expandViewButtonTapped(_ sender: ExpandablePlaneTableViewCell, for indexPath: IndexPath) {
sender.exapanded = true
tableView.reloadRows(at: [indexPath], with: .automatic)
}
and after I want to expand it and reload the cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "expandableCell", for: indexPath) as! ExpandablePlaneTableViewCell
cell.delegate = self
cell.indexPath = indexPath
return cell
}
Do not attempt to get a cell in heightForRowAt. And there certainly is no reason to do so in your case.
You seem to want the height to be one value for certain types of cells and another height for other types.
Simply use the same basic logic you have in cellForRowAt, based on the indexPath, to determine which height to return. In other words, base the decision on your data model, not on the cell.

UITableViewCells invisible

Requirement :
I have a list of UITableviewCell loaded from a nib that I'm presenting on UITableview. The first time I open the UIViewController all cells are shown correctly and work as expected.
Issue :
If I navigate back to the parent and then open the UIViewController again the UITableviewCell are 'invisible'. I say invisible because with a breakpoint in cellForRowAt I can see that the table view does load all cells and the cells are valid.
Code :
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 13
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 40
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = (project?.sliderData.sliders[indexPath.row].view)! as UITableViewCell
print(cell.contentView.subviews.count)
if let left = cell.viewWithTag(2) as? UILabel {
left.text = "left"
}
if let middle = cell.viewWithTag(3) as? UILabel {
middle.text = "middle"
}
if let right = cell.viewWithTag(4) as? UILabel {
right.text = "right"
}
return cell
}
Screen Shot Image
Expected observation :
I was thinking that maybe the subviews of the cells get released because I don't have any bindings to them in IB. To test this I'm printing the count of subviews and writing some text to the subview labels. And everything seems to go fine, the cells are loaded and the labels are there but the cells just don't show up.
But then, if I scroll the TableView up and down a little to get some cells updated those cells do appear at the top and bottom of the view as shown in the pic.
You need to call dequeueReusableCell(withIdentifier: "cell") inside your code then will show your table cell. It will reuse cell for your all numbers of row data content.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! UITableViewCell
return cell
}
More Details : How to create uitableview with multiple sections in iOS Swift.
Did not find reason why the tableView behaves the way it does so I solved the issue by dequeueing default cells. The views provided by the slider objects are added as subviews to the dequeued cells. Now the subviews can of course be any UIViews.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "sliderCell")
if cell == nil {
cell = UITableViewCell.init(style: .default, reuseIdentifier: "sliderCell")
}
cell?.addSubview((project?.sliderData.sliders[indexPath.row].view)!)
return cell!
}

Change UITableViewCell selection color based on which cell was pressed

My table view has some elements sorted by some types: TypeA, TypeB and TypeC.
I want that when I click on a cell with TypeA to change the selection color to Red, when I type on TypeB to change color to Blue and when pressing on TypeC to change color to Yellow.
Right now I came up with this code:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
guard let mode = dataSource.selectedObject else {
fatalError("willDisplayCell, but no selected row?")
}
let type = ModeType(rawValue: mode.type)!
let selectionColor = UIView() as UIView
selectionColor.backgroundColor = type.color()
cell.selectedBackgroundView = selectionColor
}
My issue with this is that willDisplayCell is called when I start my app and my data source is empty so I get a fatal error.
How can I overcome this ? Maybe using a flag to do this only when didSelectRowAtIndexPath was called.
Or is there another way to achieve what I am after ?
I assume you have created custom UITableviewCell. Create a cell type enum.
enum CellType {
case RedCell
case Yellowcell
case OrangeCell
}
//Create enum property
class CustomCell : UITableViewCell {
var cellType:CellType = CellType.RedCell //Default is RedCell
}
Now you have to assign the cell type in your ViewController tableview datasource.
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
var cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell") as! CustomCell
cell.cellType = .RedCell //your choice
return cell
}
override func tableView(tableView: UITableView, shouldHighlightRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
override func tableView(tableView: UITableView, didHighlightRowAtIndexPath indexPath: NSIndexPath) {
var cell = tableView.cellForRowAtIndexPath(indexPath)
switch(cell.cellType) {
//Handle Switch case
case .RedCell:
cell?.contentView.backgroundColor = UIColor.redColor()
cell?.backgroundColor = UIColor.redColor()
}
}
override func tableView(tableView: UITableView, didUnhighlightRowAtIndexPath indexPath: NSIndexPath) {
var cell = tableView.cellForRowAtIndexPath(indexPath)
// Set unhighlighted color
cell?.contentView.backgroundColor = UIColor.blackColor()
cell?.backgroundColor = UIColor.blackColor()
}
EDIT:
If you have created 3 different types of cell class check tableview cell class type and change the color in didHighlightRowAtIndexPath method.
My issue with this is that willDisplayCell is called when I start my
app and my data source is empty so I get a fatal error.
tableView(_:willDisplayCell:forRowAtIndexPath:) will only be called if your data source tells the table view that there are rows to display. So the problem more likely is that your tableView(_:numberOfRowsInSection:) method is returning a number larger than zero when your data source is empty.
Also, your code looks like it expects tableView(_:willDisplayCell:forRowAtIndexPath:) to get called only for selected rows. It gets called for all displayed rows. But this method isn't necessary to affect the background color. In fact, it's rarely used in most apps. There are only a few edge cases where you need to mess with the cell just before it's displayed.
The proper way to set the selection background color is to create and assign a UIView to the cell's selectedBackgroundView property. You can do that either from the cell's subclass (preferred for complex cells) or from the tableView:cellForRowAtIndexPath: data source method:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCell")
cell!.textLabel?.text = "Kilroy was here."
cell!.selectedBackgroundView = UIView(frame: cell!.bounds)
cell!.selectedBackgroundView!.backgroundColor = .greenColor()
return cell!
}

Resources