21

With this code, I can check multiple rows in a table.

But what I want is to only have one row checked at a time. Here's my code:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    UITableViewCell *theCell = [tableView cellForRowAtIndexPath:indexPath];
    
    if (theCell.accessoryType == UITableViewCellAccessoryNone) {
        theCell.accessoryType = UITableViewCellAccessoryCheckmark;
    } else if (theCell.accessoryType == UITableViewCellAccessoryCheckmark) {
        theCell.accessoryType = UITableViewCellAccessoryNone;
    }
}

If a user selects a different row, then I want the old row to simply automatically uncheck. Don't know how to do that.

pkamb
  • 33,281
  • 23
  • 160
  • 191
Jupiter869
  • 657
  • 2
  • 7
  • 19
  • 1
    For those of your questions that were answered correctly you should accept the correct answer by clicking checkmark next to it. – Rok Jarc Apr 17 '12 at 14:51

18 Answers18

88

Objective-C:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath   *)indexPath
{
     [tableView cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryCheckmark;
}

-(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath 
{
    [tableView cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryNone;
}

Swift:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}

func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
    tableView.cellForRow(at: indexPath)?.accessoryType = .none
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
Ujwal Manjunath
  • 2,970
  • 2
  • 15
  • 3
  • 3
    This doesn't take care of reusability. Careful. – p0lAris May 18 '15 at 22:24
  • agree with @p0lAris. When the cell get reused. it still having the check mark – Cullen SUN Oct 23 '15 at 15:32
  • This is fine if your table has less rows then what would fill the screen but once they spill past and start getting reused during a scroll (as @p0lAris said), your users will be very confused. – jeffjv Dec 10 '15 at 04:33
  • @HardikAmal - simple, just add the condition when configuring the cell to definitively set the accessory type. So, be sure to have the `else` after the `if` and set the value either way. – thephatp Jun 05 '16 at 21:20
27

You can create a new variable to track the index selected at didSelectRowAtIndex.

int selectedIndex;

in your cellForRowAtIndexPath

if(indexPath.row == selectedIndex)
{
   cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
else
{
   cell.accessoryType = UITableViewCellAccessoryNone;
}

and in your didSelectRowAtIndex

selectedIndex = indexPath.row;
[tableView reloadData];
John Paul Manoza
  • 1,735
  • 25
  • 22
26

The best way is to set the accessory type in cellForRowAtIndexPath.

Use didSelectRowAtIndexPath to only record which path should be selected and then call reloadData.

pkamb
  • 33,281
  • 23
  • 160
  • 191
Phillip Mills
  • 30,888
  • 4
  • 42
  • 57
  • 3
    The only problem with this solution is you will lose the deselect row animation. Just reload the other visible cells. – Nick Hingston Apr 17 '12 at 16:03
  • 1
    I definitely choose correct first and pretty later...if necessary. :-) – Phillip Mills Apr 17 '12 at 16:08
  • 1
    Thank You again! reloadData was the key to making things work. Your insight much appreciated. – Jupiter869 Apr 17 '12 at 21:09
  • 2
    Correct and pretty is achievable in this case. So there is no need to resort to `reloadData`. – Jade Jul 16 '13 at 18:54
  • 2
    You don't (and really shouldn't) call reloadData to solve this issue. Please see some of the other answers (like mine) to correctly handle the animations and reusability. – jeffjv Sep 13 '16 at 00:20
23

You don't need to (and shouldn't) just reload the table after each selection.

Apple has good documentation on how to manage a selection list. See Listing 6-3 for an example.

This is more or less the same answer as some of the others but I thought I'd add a little more detail.

What you want to do is save the current selected IndexPath to a variable and use that in didSelectRowAtIndexPath to remove the old check mark. This is also where you will be adding the new check mark.

You need to make sure to also set/unset the checkmarks in cellForRowAtIndexPath otherwise if your list is large and cells are reused it will look like multiple rows are selected. This is a problem with some of the other answers.

See swift 2.0 example below:

// for saving currently seletcted index path
var selectedIndexPath: NSIndexPath? = NSIndexPath(forRow: 0, inSection: 0)  // you wouldn't normally initialize it here, this is just so this code snip works
// likely you would set this during cellForRowAtIndexPath when you dequeue the cell that matches a saved user selection or the default

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    // this gets rid of the grey row selection.  You can add the delegate didDeselectRowAtIndexPath if you want something to happen on deselection
    tableView.deselectRowAtIndexPath(indexPath, animated: true) // animated to true makes the grey fade out, set to false for it to immediately go away

    // if they are selecting the same row again, there is nothing to do, just keep it checked
    if indexPath == selectedIndexPath {
        return
    }

    // toggle old one off and the new one on
    let newCell = tableView.cellForRowAtIndexPath(indexPath)
    if newCell?.accessoryType == UITableViewCellAccessoryType.None {
        newCell?.accessoryType = UITableViewCellAccessoryType.Checkmark
    }
    let oldCell = tableView.cellForRowAtIndexPath(selectedIndexPath!)
    if oldCell?.accessoryType == UITableViewCellAccessoryType.Checkmark {
        oldCell?.accessoryType = UITableViewCellAccessoryType.None
    }

    selectedIndexPath = indexPath  // save the selected index path

    // do whatever else you need to do upon a new selection
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
    // if this is the currently selected indexPath, set the checkmark, otherwise remove it
    if indexPath == selectedIndexPath {
        cell.accessoryType = UITableViewCellAccessoryType.Checkmark
    } else {
        cell.accessoryType = UITableViewCellAccessoryType.None
    }
}
jeffjv
  • 3,461
  • 2
  • 21
  • 28
  • This answer is amazing but it assumes that `selectedIndexPath` is initialized, doesn't it? I mean, if there is no default value when initializing the table view, `didSelectRowAtIndexPath` should try to unwrap `selectedIndexPath` and set the value – matteodv Aug 31 '17 at 14:54
  • @matteodv, in my code snip I have a comment on the third line that says `// likely you would set this during cellForRowAtIndexPath when you dequeue the cell that matches a saved user selection or the default`. Does that answer your question? – jeffjv Sep 01 '17 at 20:13
  • Yes, sorry... I meant a situation in which there isn't a default value and it need the be set the first time a row is clicked. In this case, cellForRowAtIndexPath won't set the variable... – matteodv Sep 01 '17 at 21:57
  • Isn't calling `tableView.cellForRowAtIndexPath( ... )` essentially the same as calling `tableView.reloadRows(at: ...)`? If you reload the old and new rows (not the entire table view) you can remove the duplicated checkmark code from `didSelect`. – pkamb Oct 04 '21 at 20:23
8

You don't need to reload the tableView.

See the below code:

Declare a NSIndexPath lastSelectedIndexPath variable for your class

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    if(lastSelectedIndexPath) {

        UITableViewCell *lastCell = [tableView cellForRowAtIndexPath:lastSelectedIndexPath];
        [lastCell setAccessoryView:nil];
    }

    UITableViewCell *currentCell = [tableView cellForRowAtIndexPath:currentIndexPath];
    [currentCell setAccessoryView:UITableViewCellAccessoryCheckmark];

    lastSelectedIndexPath = indexPath;
}
Burhanuddin Sunelwala
  • 5,318
  • 3
  • 25
  • 51
  • Does anyone know how to save the state of the selected row using NSUserDeafults? I've been looking for a simple way to do this. All the solution I've found are rather elaborate, and for my project that would simply clutter up my code. –  Mar 08 '14 at 06:31
  • @Borges could you explain your question a bit more? what exactly you mean by state of a cell? – Burhanuddin Sunelwala Mar 09 '14 at 07:28
5

For Swift 3 following worked for me :

override func viewDidLoad() {
    super.viewDidLoad()

    // allow cells to be selected
    tableView.allowsSelection = true

    // ensure that deselect is called on all other cells when a cell is selected
    tableView.allowsMultipleSelection = false
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableVIew.cellForRow(at: indexPath as IndexPath)?.accessoryType = .checkmark
}

func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
    tableVIew.cellForRow(at: indexPath as IndexPath)?.accessoryType = .none
}
Brian Li
  • 2,260
  • 1
  • 15
  • 18
4

I think https://stackoverflow.com/a/5960016/928599 will help you.
I used it and it Works with deselectRowAtIndexPath too!

Community
  • 1
  • 1
mohsinj
  • 143
  • 2
  • 9
4

Simplest way is to save the selected IndexPath and check it in cellForRowAtIndexPath:

  1. Initialize the selected index path:

    selectedIndexPath = [[NSIndexPath alloc] init];
    
  2. In cellForRowAtIndexPath:

    if([selectedIndexPath isEqual:indexPath]){
        [checkMark setHidden:NO];
    } else {
        [checkMark setHidden:YES];
    }
    
  3. In didSelectRowAtIndexPath:

    selectedIndexPath = indexPath;
    [tableView reloadData];
    

It should work work try this Njoy :)

pkamb
  • 33,281
  • 23
  • 160
  • 191
1

Try this:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    if indexPath.section == 1 {
        // un-select the older row
        if let selected = self.LastSelected {
            tableView.cellForRowAtIndexPath(selected)?.accessoryType = .None
        }

        // select the new row
        tableView.cellForRowAtIndexPath(indexPath)?.accessoryType = UITableViewCellAccessoryType.Checkmark
        self.LastSelected = indexPath
    }
}
alcedo
  • 1,632
  • 2
  • 12
  • 14
1

In Swift, following works perfectly:

lastSelectedIndexPath = NSIndexPath(forRow: 1, inSection: 0)//Row will be the previous selected row


    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
    let cell:LabelsSelectionCell = tableView.dequeueReusableCellWithIdentifier("LabelsSelectionCell", forIndexPath: indexPath) as! LabelsSelectionCell
    cell.setCellLael(labelOptionsArray[indexPath.row])
    if lastSelectedIndexPath == indexPath
    {
        cell.setCellCheckedStatus(true)
    }
    return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
    if let _ = lastSelectedIndexPath
    {
        let lastCell:LabelsSelectionCell = tableView.cellForRowAtIndexPath(lastSelectedIndexPath!) as! LabelsSelectionCell
        lastCell.setCellCheckedStatus(false)
    }
    let currentCell:LabelsSelectionCell = tableView.cellForRowAtIndexPath(indexPath) as! LabelsSelectionCell
    currentCell.setCellCheckedStatus(true)

    lastSelectedIndexPath = indexPath

    tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
msmq
  • 1,298
  • 16
  • 28
0

Swift iOS

 var checkedIndexPath : NSIndexPath?

// table row which row was selected
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
    
    println("Section #\(indexPath.section)! You selected cell #\(indexPath.row)!")
    
    if (self.checkedIndexPath != nil) {
        var cell = tableView.cellForRowAtIndexPath(self.checkedIndexPath!)
        cell?.accessoryType = .None
    }
    
    var cell = tableView.cellForRowAtIndexPath(indexPath)
    cell?.accessoryType = .Checkmark
    
    self.checkedIndexPath = indexPath
    
}// end tableView
pkamb
  • 33,281
  • 23
  • 160
  • 191
Vinod Joshi
  • 7,696
  • 1
  • 50
  • 51
  • It is crashing when 'checkedIndexPath' is not in the VisibleCells in the tableView if (self.checkedIndexPath != nil) { var cell = tableView.cellForRowAtIndexPath(self.checkedIndexPath!) cell?.accessoryType = .None } Cell is nil if it is not in the Visible Cells – umakanta Sep 15 '15 at 12:34
0

In Swift 2.0 I used this:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

    if((lastSelectedIndexPath) != nil) {
        let lastCell = tableView.cellForRowAtIndexPath(lastSelectedIndexPath)
        lastCell?.accessoryType = UITableViewCellAccessoryType.None
    }

    let currentCell = tableView.cellForRowAtIndexPath(indexPath)
    currentCell?.accessoryType = UITableViewCellAccessoryType.Checkmark

    lastSelectedIndexPath = indexPath

}
Dasoga
  • 5,489
  • 4
  • 33
  • 40
  • how is lastSelectedIndexPath initialised and managed? as lastSelectedIndexPath can never be nil – Deepak Thakur Mar 02 '16 at 07:37
  • var lastSelectedIndexPath: NSIndexPath? = NSIndexPath(forRow: 0, inSection: 0) – Deepak Thakur Mar 02 '16 at 08:34
  • `var lastSelectedIndexPath: NSIndexPath!` Is nil at start, but after, I check if is nil with the if, and at end of the function I do: `lastSelectedIndexPath = indexPath` – Dasoga Mar 02 '16 at 14:42
0

My way is deselect other cells during selecting:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = ....

    if condition {
        cell.accessoryType = .checkmark
        tableView.selectRow(at: indexPath, animated: false, scrollPosition: UITableViewScrollPosition.bottom)
    } else {
        cell.accessoryType = .none
    }

    return cell
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    for row in 0...tableView.numberOfRows(inSection: 0) {
        if row == indexPath.row {continue}
        tableView.deselectRow(at: IndexPath(row: row, section: 0), animated: true)
    }

    if let cell = tableView.cellForRow(at: indexPath) {
        cell.accessoryType = .checkmark
    }
}

func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
    let cell = tableView.cellForRow(at: indexPath)
    cell?.accessoryType = .none
}
protspace
  • 2,047
  • 1
  • 25
  • 30
0

Here's what I came up when I had this problem.
This code is implemented in the latest version of Swift, as of today...
For more info and/or the extension file, please check out the Github Gist of this snippet.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {       
    if let sip = selectedIndex {
        tableView.deselectRow(at: sip, animated: false)
    }
    selectedIndex = indexPath
}

override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
    if selectedIndex?.row == indexPath.row {
        selectedIndex = nil
    }
}
0

tested and solved try this its worked perfectly

add this as global variable

var selectedIndexPath = NSIndexPath(row: 0, section: 0)

Add this in didselect method

        // old one check off
        if indexPath != selectedIndexPath as IndexPath {
            let oldCell = categorytable.cellForRow(at: selectedIndexPath as IndexPath)
            if oldCell?.accessoryView != nil {
                oldCell?.accessoryView = nil
            }
            else {
                let imageName = "checkmark"
                let image: UIImageView = UIImageView(image: UIImage(named: imageName))
                cell.accessoryView = image
            }
        }
        // the new one on

        if  cell.accessoryView == nil {
            let imageName = "checkmark"
            let image: UIImageView = UIImageView(image: UIImage(named: imageName))
            cell.accessoryView = image
        }
        else {
            cell.accessoryView = nil
        }
hardik
  • 199
  • 1
  • 6
  • 21
0

You can do it in custom cell type in one line of code.

final class CheckableTableViewCell: UITableViewCell {

  override func setSelected(_ selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)

    self.accessoryType = selected ? .checkmark : .none
  }

}

To make this method work - your cell should be selected.

If you don’t want to see the highlight of the selected cell, just set cell's selectionStyle to .none in storyboard or in code

This method will not work, if you will call

tableView.deselectRow(at: indexPath, animated: true)

Also works great with multiple selection.

0

Swift 5 version of jeffjv's response.

// for saving currently selected index path
var selectedIndexPath: NSIndexPath? = NSIndexPath(forRow: 0, inSection: 0)  // you wouldn't normally initialize it here, this is just so this code snip works
// likely you would set this during cellForRowAtIndexPath when you dequeue the cell that matches a saved user selection or the default

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) {
    // this gets rid of the grey row selection.  You can add the delegate didDeselectRowAtIndexPath if you want something to happen on deselection
    tableView.deselectRow(at: indexPath, animated: true) // animated to true makes the grey fade out, set to false for it to immediately go away

    // if they are selecting the same row again, there is nothing to do, just keep it checked
    if indexPath == selectedIndexPath! as IndexPath {
        return
    }

    // toggle old one off and the new one on
    let newCell = tableView.cellForRow(at: indexPath)
    if newCell?.accessoryType == UITableViewCell.AccessoryType.None {
        newCell?.accessoryType = UITableViewCell.AccessoryType.Checkmark
    }
    let oldCell = tableView.cellForRow(at: selectedIndexPath! as IndexPath)
    if oldCell?.accessoryType == UITableViewCell.AccessoryType.Checkmark {
        oldCell?.accessoryType = UITableViewCell.AccessoryType.None
    }

    selectedIndexPath = indexPath as NSIndexPath  // save the selected index path

    // do whatever else you need to do upon a new selection
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
    // if this is the currently selected indexPath, set the checkmark, otherwise remove it
     if indexPath == selectedIndexPath! as IndexPath {
        cell.accessoryType = UITableView.CellAccessoryType.Checkmark
    } else {
        cell.accessoryType = UITableView.CellAccessoryType.None
    }
}
0

Store the selected index path, update it in didSelectRowAt:, and from there call reloadRows(at: to reload only the old and new checked rows (not the entire table view):

var selectedIndexPath: IndexPath?

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let previous = selectedIndexPath
    selectedIndexPath = indexPath
    
    let allowDeselection = true // optional behavior
    if allowDeselection && previous == selectedIndexPath {
        selectedIndexPath = nil
    }
    
    tableView.reloadRows(at: [previous, selectedIndexPath].compactMap({ $0 }), with: .automatic)
    tableView.deselectRow(at: indexPath, animated: true)
}

Handle the setting of the checkmark accessory view in cellForRowAt indexPath:. This will account for cell reuse and also be triggered by your above calls to reloadRows(at:, avoiding duplicating the code of setting the cell accessory:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "IDENTIFIER", for: indexPath)
    
    cell.accessoryType = (indexPath == selectedIndexPath) ? .checkmark : .none
    
    // ...
    
    return cell
}
pkamb
  • 33,281
  • 23
  • 160
  • 191