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.
Related
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)
}
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.
UITableView allows you to assign an accessory -- like a button -- that shows on the right side of the cell (documentation).
In my audio app, I will want to allow a user to download by clicking the accessory and then show it succeeded with a checkmark. To test this now, I just want the accessory detail to turn into a checkmark after it's tapped.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = TableData[indexPath.row]
// this works, adds a button to right cell
cell.accessoryType = .detailDisclosureButton
return cell
}
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
// doSomethingWithItem(indexPath.row)
// my logic: this function applies when button is tapped. I try to reset the accessory type to checkmark when clicked
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.accessoryType = .checkmark
}
This will build, but it results in a failure starting with *** Assertion failure in -[UITableView _dequeueReusableCellWithIdentifier:forIndexPath:usingPresentationValues:],
I'm new to Swift, but this seemed logical: set a function to take an action when the accessory is clicked and then set the accessory to a checkmark (one of the accessory types).
Is there a better way to do this?
A note: I do not want to use a segue or move to a different VC; I'd like this to update the UI within the table view.
Instead of using dequeReusableCell you should be using cellForRow in your accessoryButtonTappedForRow function.
Read the documentation to see the difference between those two functions and look a little further into how cell reuse works. cellForRow(at:) returns the specific UITableViewCell, which is what you are looking for in this situation.
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
// doSomethingWithItem(indexPath.row)
// cellForRow instead of dequeue
let cell = tableView.cellForRow(at: indexPath)
cell.accessoryType = .checkmark
}
In addition, make sure you are registering your cell with that same reuse identifier of "cell" with either registerClass or registerNib
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()
}
I have a VERY annoying problem.
I have a tableView with more or less 50 cells, displaying some options to which I can select the ones I want. I read in Apple's documentation that by default the cells are reused when they are not displayed. With this, if I select the first cell, every 6 cells 1 is marked, that is, if I select the first 6 cells, ALL cells in the table are marked!
My table view allows multiple selections. The selection is being made like this:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.checkmark
}
How do I solve this? I know you have such a "prepareForReuse ()" for the subclass, would that be the solution? If so, can you give me an example of how you would do it?
here is code it may help you
var arr_selectedindePath = NSMutableArray()
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
if arr_selectedindePath .contains(indexPath) {
arr_selectedindePath .remove(indexPath)
tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.none
}
else
{
arr_selectedindePath .add(indexPath)
tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.checkmark
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell")!
if arr_selectedindePath .contains(indexPath) {
cell.accessoryType = .checkmark
}
else
{
cell.accessoryType = .none
}
}
You will need to update your data model accordingly so that when cellForRowAtIndexPath is called it displays cells with updated content.
If you don't use a datamodel you will need to store the indexPath's in a mutable array and make a check whether the current indexPath is marked or not.
Hope this helps.
In your custom cell
1.Create a delegate
protocol CellDelegate {
func didTapOnButton(_ cell:Cell)
}
2. Declare delegate
var delegate:CellDelegate?
3.Override this method
override func prepareForReuse() {
super.prepareForReuse()
self.delegate = nil
}
#IBAction func buttonTapped()
{
self.delegate!.didTapOnButton(self)
}
In your tableview controller
1.Implement delegate method
2.Inside cellForRowAtIndexPath assign tag value to cell.button
3.implement this method
func didTapOnButton(_ cell: Cell) {
print("off button clicked at index \(cell.button.tag)")
}