Swift 5 | didSelectRowAt is selecting two cells at the same time - ios

I am doing a screen where there a list o cells with a switch, like an image below;
I have a struct where a save the label of the cell and the switch state value. This struct is loaded at: var source: [StructName] = [] and then source values are attributed to the UITableView cells.
The problem is that when a touch a cell the function: func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) change multiples cells switches states at the same time.
I try to work around the problem by implementing the following function:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let cell = tableView.cellForRow(at: indexPath) as! CustomTableViewCell
for n in 0..<source.count{ // This loop search for the right cell by looking at the cell label text and the struct where the state of the switch is saved
if cell.label.text! == source[n].Label{
// If the label text is equal to the position where the values is saved (is the same order that the cells are loaded in the UITableView) then a change the state of the switch
let indexLabel = IndexPath(row: n, section: 0)
let cellValues = tableView.cellForRow(at: indexLabel) as! CustomTableViewCell
if cellValues.switchButton.isOn {
cellValues.switchButton.setOn(false, animated: true)
source[n].valor = cellValues.switchButton.isOn
} else {
cellValues.switchButton.setOn(true, animated: true)
source[n].valor = cellValues.switchButton.isOn
}
break
}
}
although is saved the right values to the switch state array(source) the visual state of multiples switches also changes even though the cells where never touch.
How could I change my code to select and change only the touched cell?

You should not store / read the state of anything in a cell.
But first things first:
Why do loop through all the values? You should be able to access the row in the data model directly by indexPath.row
You should only modify the model data, not the cell
You then tell the table view to reload the cell, which will then ask the model for the correct data to be displayed.
I would suggest the following:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let row = indexPath.row
source[row].valor.toggle()
tableView.reloadRows(at:[indexPath], with:.automatic)
}

Related

How to create clickable cells in UITableView (Swift)

I'm working on a personal project and I want to make the cells in my UITableView clickable, such that when they are pressed they segue to the next view, and pass information from their cell along with it when they are pressed.
I've attempted to look over other guides and videos that demonstrate how to do this, however they are all outdated. One that stood out to me that seemed to show promise but didn't end up working was in reference to a certain "didSelectRowAt" function, but I could not get this to work.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as!
CandidateTableViewCell
if (indexPath.row >= candidateCells.count){
print("???")
candidateCells.append(cell)
print("Strange error")
}
tableView.delegate = self
cell.CandidateName?.text = candidatesNames[indexPath.item]
cell.CandidatePos?.text = candidatesPositions[indexPath.item]
//cell.candidateButton.tag = indexPath.row
cell.CandidateName.sizeToFit()
cell.CandidatePos.sizeToFit()
print(candidateCells)
print(indexPath.row)
print(candidateCells.count)
print(indexPath.row >= candidateCells.count)
candidateCells[indexPath.item] = cell
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "Segue", sender: Any?.self)
}
What I was expecting to happen was that the cell would become clickable, and when the cell is clicked it would send me to the next page in the app, however when clicked, nothing happens. The cell does not become highlighted, and the segue does not occur. Thank you so much for any and all suggestions!
If you want to pass information to the next cell, I usually avoid using a Segue, and setup the ViewController with a reuse identifier. By doing this, you can pass information easier. For instance
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = self.storyboard?.instantiateViewController(withIdentifier: "YOURRESUEIDENTIFIER") as! YOURVIEWCONTROLLERCLASS
vc.infoToPass = cellData[indexPath.row]
DispatchQueue.main.async {
self.navigationController?.pushViewController(vc, animated: true)
}
}
Your other option would be to implement the shouldPerformSegueWithIdentifier() method in order to set the data properly. You would click your cell, which calls the performSegue function, which would call the shouldPerformSegueWithIdentifier where you can set the data and return true

As UITableviewCell count more than 10 and if I click on top of button it automatically click in last of UITableViewCell button. any idea?

In my UITableViewCell have button AddToCart buttons. As if my UITableView data is more than 10 means I have to scroll to see all data. So now if I will on first button of first UITableViewCell as I scroll down to see all records of tableview than automatically last or second last button will also click I am unable to find the problem why this is happening
I am implementing first time this type of functionality so got stuck to resolve the problem
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 13
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tblCell") as! ProductTableViewCell
cell.btnAddToCart.tag = indexPath.row
cell.btnAddToCart.addTarget(self, action: #selector(addToCartDell(sender:)), for: .touchUpInside)
return cell
}
This function is used for hide and show Add To Cart button option.
#objc func addToCartDell(sender: UIButton) {
let tagVal = sender.tag
let indexPath = IndexPath(item: tagVal, section: 0)
if let cell = tblProduct.cellForRow(at: indexPath) as? ProductTableViewCell {
cell.btnAddToCart.isHidden = true
}
}
Cells are reused. You don't save the hidden state of the cell so when a cell is reused the latest state is preserved.
In Swift the most efficient and reliable solution is to save the state added to cart in the data model and use a callback closure to update the UI in cellForRow.
In the data model add a property addedToCart, it's assumed that a custom struct or class is used as data model
var addedToCart = false
In ProductTableViewCell add the callback variable and an IBAction. Connect the IBAction to the button
var callback : (()->())?
#IBAction func buttonPressed(_ sender : UIButton) {
callback?()
}
In the controller in cellForRow handle the callback, products represents the data source array
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tblCell") as! ProductTableViewCell
let product = products[indexPath.row]
cell.btnAddToCart.isHidden = product.addedToCart
cell.callback = {
product.addedToCart = true
cell.btnAddToCart.isHidden = true
}
return cell
}
No tags, no target/action, no protocols, no extra work in willDisplayCell .
This issue is turning up because we re-user same cell for displaying any further rows that was not visible yet.
you may implement this method to correct the display of any further cells
optional func tableView(_ tableView: UITableView,
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath)
Customize the cell as you want it to appear in this delegate method. This delegate method is called just before the cell is displayed, so you can do any customization here and it will turn up in the UI as per your customization.
If we go deep in to implementation.
There must be a model that keeps the state addedToCart in this model on basis of the button tapped in a particular row and use this same model's addedToCart (model.addedToCart) to show hide the button in delegate method.

TableView Cell not loading View Controller until Different TableView Cell is clicked

I've searched around and haven't truly found why this is happening. Basically, I followed this tutorial https://www.youtube.com/watch?v=yupIw9FXUso by Jared Davison on creating Table View Cells to Multiple View controllers. In his example, everything works perfectly, but for some reason when you click on a table view cell in my code the cell is highlighted in grey. Then, when the user clicks on a separate table view cell the view controller that should have been loaded by the first table view cell is loaded. If the user then clicks back on the original table view cell the page that should have been loaded by the second table view cell is loaded. In summary, all of the view controllers are loaded a "click" behind.
Here is the code for the table view:
//Feed Navigation Functions
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return elements.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 75
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "feedCell") as! FeedTableViewCell
cell.txtTitle.text = "The Fight Against \(elements[indexPath.row])"
cell.issueIcon.image = UIImage(named: "Issue Icons_\(elements[indexPath.row])")
return cell
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let vcName = identities[indexPath.row]
let viewController = storyboard?.instantiateViewController(withIdentifier: vcName)
self.navigationController?.pushViewController(viewController!, animated: true)
}
Update: The array is a simple array of strings for example [One, Two, Three] there are 6 strings in the array.
When you select a cell then select another one the method didDeselectRow is called so the Vc is pushed you actually want to implement didSelectRowAt
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vcName = identities[indexPath.row]
let viewController = storyboard?.instantiateViewController(withIdentifier: vcName)
self.navigationController?.pushViewController(viewController!, animated: true)
// this to deSelect it after push
tableView.deselectRow(at: indexPath, animated: false)
}
This method fired when a cell is selected
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath
This method fired when a cell is deSelected
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath
You want to use the tableView's delegate method didSelectRow instead of didDeselectRow I think...
Issue: click on a table view cell in my code the cell is highlighted in grey
This is due to the selectionStyle, which you can read about here. If you don't want the cell highlighted, you can set cell.selectionStyle = .none.
Edit: As indicated in other correct response - issue was with incorrect/typo in method - we should use didSelectRowAt not didDeselectRowAt.

How to find out which section the static table view cell belongs to?

I have a static table view with different sections. Here you can see one of the sections:
Users can tap the cells in these sections to check them, but I want them not to be able to tap two cells from the same section, for example, if they have tapped the cell named "Name A-Z", then they tap the one named "Name Z-A", the first one gets unchecked, and the second one gets checked. To do so, I think, I should somehow check whether two cells are from the same section or not, but I don't know how to implement that. I'm using
tableView(_ didSelectRowAt:)
method for selecting the cells, but I don't know how to access cells' sections from there. Maybe I should use another method? Any ideas how to implement this?
Okay so the code below lets you do a specific action whenever a cell is selected. The code recognizes what section it is in automatically as well.
Currently the table updates the label to say selected and automatically sets the other two to N/A. You can replace this with your own code to hide/show a checkmark or something.
Here is a video of what the following code does as well.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.section {
case 0:
switch indexPath.row {
case 0:
sectionOneCellOne.textLabel?.text = "SELECTED"
sectionOneCellTwo.textLabel?.text = "N/A"
sectionOneCellThree.textLabel?.text = "N/A"
case 1:
sectionOneCellOne.textLabel?.text = "N/A"
sectionOneCellTwo.textLabel?.text = "SELECTED"
sectionOneCellThree.textLabel?.text = "N/A"
case 2:
sectionOneCellOne.textLabel?.text = "N/A"
sectionOneCellTwo.textLabel?.text = "N/A"
sectionOneCellThree.textLabel?.text = "SELECTED"
default:
break
}
case 1:
switch indexPath.row {
case 0:
sectionTwoCellOne.textLabel?.text = "SELECTED"
sectionTwoCellTwo.textLabel?.text = "N/A"
sectionTwoCellThree.textLabel?.text = "N/A"
case 1:
sectionTwoCellOne.textLabel?.text = "N/A"
sectionTwoCellTwo.textLabel?.text = "SELECTED"
sectionTwoCellThree.textLabel?.text = "N/A"
case 2:
sectionTwoCellOne.textLabel?.text = "N/A"
sectionTwoCellTwo.textLabel?.text = "N/A"
sectionTwoCellThree.textLabel?.text = "SELECTED"
default:
break
}
default:
break
}
}
The sectionOneCellOne variables and such are just individual cells defined in the View Controller.
In func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) you can get the section using indexPath.section. Store the indexPaths of selections in an array and check for and replace the indexPath with the same section when one is encountered inside func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath).
Like this:
var selectedIndexPaths:[IndexPath] = []
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){
if let sameSectionPathIndex = selectedIndexPaths.index(where:{$0.section == indexPath.section}){
if let previousSelectedCell = tableView.cellForRow(at:selectedIndexPaths[sameSectionPathIndex]){
//Do unchecking and anything else with the previously selected cell
}
selectedIndexPaths.remove(at: sameSectionPathIndex)
selectedIndexPaths.insert(indexPath, at: sameSectionPathIndex)
} else {
selectedIndexPaths.append(indexPath)
}
if let toBeSelectedCell = tableView.cellForRow(at: indexPath){
// Do checking and anything else you want with the newly selected cell
}
}
You could do exactly what lufritz said.
Create a variable in your viewController to have your selected index
var selectedIndex = -1
you can set the value with -1, because 0 is the first cell of the table.
i dont know how you want to make the select effect, in this example i just change the background color, created two functions for select and deselect the cells:
func selectCell(cell:UITableViewCell){
cell.backgroundColor = .green
}
func deselectCell(cell:UITableViewCell){
cell.backgroundColor = .white
}
After that in your cellForRowAt indexPath: method you can do something like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "Cell"){
cell.textLabel?.text = "Example \(indexPath.row)"
deselectCell(cell: cell)
if selectedIndex == indexPath.row{
selectCell(cell: cell)
}
return cell
}
return UITableViewCell.init()
}
if the selectedIndex is the current indexPath.row you use the selectCell method, but first you have to use the deselect method, for deselect the last one.
the last thing is set the selectedIndex variable, like you thought, put it in didSelectRowAt indexPath method:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedIndex = indexPath.row
tableView.reloadData()
}

swift 3 - ViewController not deselecting selected rows in a UITableView

I have a tableview where the user is able to select multiple rows (these rows are distinguished by a checkbox in the row). For some reason, however, I can't implement the functionality to deselect any selected row. Can somebody tell me what I'm missing?
SomeViewController.m
#objc class SomeViewController: UIViewController, NSFetchedResultsControllerDelegate, UITableViewDataSource, UITableViewDelegate {
var deviceArray:[Device] = []
// [perform a fetch]
// [insert fetch results into deviceArray to be displayed in the tableview]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customcell", for:
indexPath)
// Set up the cell
let device = self.deviceArray[indexPath.row]
cell.textLabel?.text = device.name
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView(tableView, cellForRowAt: indexPath).accessoryType = UITableViewCellAccessoryType.checkmark
NSLog("Selected Row")
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
self.tableView(tableView, cellForRowAt: indexPath).accessoryType = UITableViewCellAccessoryType.none
NSLog("Deselected Row")
}
}
More info:
Looking at the debug logs inserted, I can share the following observations:
When selecting an unselected row, the console prints "Selected Row"
If I click the same row from observation #1, the console prints "Selected Row" only
If I click on any other row, the console prints "Deselected Row" and then "Selected Row"
If I click on the same row as observation #3, the console prints "Selected Row" only.
So, it looks like everytime I click on a different row, tableView: didDeselectRowAt: gets called; however, checkmarks in the clicked row do not go away.
More Info 2:
So I'm new to storyboards and didn't set the "allowsMultipleSelection" property. Going into the Attributes Inspector, this is what my settings look like:
Now, when pressing the same row in the tableView, my console confirms that the app is alternating between tableView:didSelectRowAt: and tableView:didDeselectRowAt:, however, the checkmark doesn't disappear; once the user selects a row, the checkmark remains selected even when tableView:didDeselectRowAt: is called. What else am I missing?
First off make sure your datasource AND delegate outlets are set if you are setting them from storyboard.
Another thing is you need to set allowsMultipleSelection property to true to get the didSelect, didDeselect methods to get called in the behavior you want. Otherwise it will always call didSelect for the cell you tapped on and didDeselect for the most previously selected cell.
The other thing to note is that you are referencing self.tableView when setting your cell.accessoryType property. This may be different instance of the tableView being passed into the delegate method. I recommend a guard let statment to ensure the code setting the accessory type only applies if the tableView being passed into the function. Here is code I used to get it to work.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//Notice I use tableView being passed into func instead of self.tableView
guard let cell = tableView.cellForRow(at: indexPath) else {
return
}
cell.accessoryType = .checkmark
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) else {
return
}
cell.accessoryType = .none
}
If you want user to be able to select multiple cells, you need to set tableView.allowsMultipleSelection = true in the viewDidLoad method or set multiple selection in the attributes inspector.

Resources