How to convert multiple cell selection to single cell selection using Swift - ios

In myscenario, I am trying to create single cell selection checkmark at a time. I used below code for multiple cell selection with isSelected Bool value for selection cell persistent. Now, how to convert below code for single cell selection.
My Code Below
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
let item = self.titleData[indexPath.row]
cell.textLabel?.text = item.title
cell.accessoryType = item.isSelected ? .checkmark : .none
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
titleData[indexPath.row].isSelected.toggle()
tableView.reloadRows(at: [indexPath], with: .none)
let selectedTitle = titleData.filter{$0.isSelected}
print("\(selectedTitle)")
}

First, in viewDidLoad(), make your tableView to allow single selection only. like this:
yourTableView.allowsMultipleSelection = false
then you can use didSelectRowAt and didDeselectRowAt for this. This will enable only one selection at a time.
// assign isSelected true and accessoryType to checkmark
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
self.titleData[indexPath.row].isSelected = true
let selectedTitle = self.titleData[indexPath.row].title
cell.accessoryType = .checkmark
}
// assign isSelected false and accessoryType to none
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
self.titleData[indexPath.row].isSelected = false
cell.accessoryType = .none
}

You need to maintain global variable because if you want to manage using your array you need to reset isSelected bit of array every time before you do selection.
var isSelected = false
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
let item = self.titleData[indexPath.row]
cell.textLabel?.text = item.title
cell.accessoryType = isSelected ? .checkmark : .none
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
isSelected = true
tableView.reloadData()
}

Related

Getting label text when Cell is Selected in TableView (Swift)

I have the below code and I want to print the text of the selected cell ( a custom cell with a text label )
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DateCell", for: indexPath) as! DateCell
cell.dateLabel.text = objectsArray[indexPath.section].sectionObjects[indexPath.row]
cell.selectionStyle = .none
contentView.separatorStyle = .singleLine
contentView.allowsSelection = true
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if contentView.cellForRow(at: indexPath)?.accessoryType == UITableViewCell.AccessoryType.none{
contentView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
else{
contentView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
}
I already tried adding the below code in didSelect row at but I get nil.
print ((contentView.cellForRow(at: indexPath)?.textLabel?.text)!)
Any ideas on how I can do this?
Get it from the original source...
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
let str = objectsArray[indexPath.section].sectionObjects[indexPath.row]
print(str)
}
Since you are setting text from the data source, when cell is selected, you can check the index in your data source
if let text = objectsArray[indexPath.section].sectionObjects[indexPath.row]{
//Do Something
}

Swift UITableview cell checkmark with selection blink option

My scenario, I am trying to create UITableview single cell checkmark selection and Multiple cell check mark selection. Here, I am trying by below code for single selection check mark but Its reusability not working. Also, I don't know how to do multiple cell selection by clicking cell section.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView.deselectRow(at: indexPath, animated: true)
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = .none
}
For reusability, you need a variable to save the selected cell index(or array for cells) and then set the appropriate accessoryType when displaying the cell.
For single selection, remember to clear the previous selection.
A sample pattern for single selection:
var selectedRow = 0
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedRow = indexPath.row
for cell in tableView.visibleCells { //Why not using didDeselectRowAt? Because the default selected row(like row 0)'s checkmark will NOT be removed when clicking another row at very beginning.
cell.accessoryType = .none
}
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.accessoryType = indexPath.row == selectedRow ? .checkmark : .none
}
A pattern for multiple selections (Updated for multi-sections):
var selectedIndexPaths = Set<IndexPath>()
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if selectedIndexPaths.contains(indexPath) { //deselect
selectedIndexPaths.remove(indexPath)
tableView.cellForRow(at: indexPath)?.accessoryType = .none
}
else{
selectedIndexPaths.insert(indexPath) //select
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.accessoryType = selectedIndexPaths.contains(indexPath) ? .checkmark : .none
}
Model
class Item {
var name:String!
var isSelected = false
}
then inside cellForRowAt
cell.accessoryType = items[indexPath.row].isSelected ? .checkmark : .none
inside didSelectRowAt
items[indexPath.row].isSelected.toggle()
tableView.reloadRows(at:[indexPath],with:.none)
Set isSingleSelection to false for multiple selections and to true for single selection
var isCellSelected = [Bool]()
var isSingleSelection = false
override func viewDidLoad() {
super.viewDidLoad()
for _ in arrayFillingYourTableview { isCellSelected.append(false) }
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = branchTbView.dequeueReusableCell(withIdentifier: "cell")!
cell.accessoryType = isCellSelected[indexPath.row] ? .checkmark : .none
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if isSingleSelection {
for i in 0..<isBranchSelected.count {
isCellSelected[i] = false
}
}
isCellSelected[indexPath.row] = ! isCellSelected[indexPath.row]
tableView.reloadData()
}
}

How to have checkmarks icons (accessory view) on tableview already checked after cells rendered?

I have model data, which has a boolean value that says if checkmark should be shown (accessoryType is .checkmark)...
So for example, I want to have two of five rows checkmarked at start (based on my model as I said)... The thing is, I am able to show checkmarks, but after I tap on them, toggling doesn't work right:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .checkmark
model[indexPath.row].isCellSelected = true
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .none
model[indexPath.row].isCellSelected = false
}
}
And here is a cellForRowAt:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = model[indexPath.row]
let identifier = data.subtitle != nil ? kSubtitleID : kNormalID
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
cell.textLabel?.text = data.title
cell.detailTextLabel?.text = data.subtitle
return cell
}
I am able to show check mark like this:
cell.accessoryType = data.isCellSelected ? .checkmark : .none
But when I tap on it, it cause it is selected (allowsMultipleSelection is set to true), it doesn't get toggled, but rather stays for the first time.
Here is the model I use. It is really simple:
struct CellModel{
var title:String?
var subtitle:String?
var isCellSelected:Bool = false
}
You should perform toggling in tableView:didSelectRowAt: only, and reload the necessary cell afterwards. You should omit tableView:didDeselectRowAt:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
// Toggle the value - true becomes false, false becomes true
model[indexPath.row].isCellSelected = !model[indexPath.row].isCellSelected
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .none)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = model[indexPath.row]
let identifier = data.subtitle != nil ? kSubtitleID : kNormalID
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
cell.textLabel?.text = data.title
cell.detailTextLabel?.text = data.subtitle
// Set the checkmark or none depending on your model value
cell.inputAccessoryType = data.isCellSelected ? .checkmark : .none
return cell
}
Edit:
Use this for single selection only + ability to deselect selected item:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Loop through all model items
for (index, item) in model.enumerated() {
if (index == indexPath.row) {
// Toggle tapped item
item.isCellSelected = !item.isCellSelected
} else {
// Deselect all other items
item.isCellSelected = false
}
}
tableView.reloadData();
}
You dont have to use select and deselect until you have a model that saves the boolean value
In tableview didselect method
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if model[index.row].isCellSelected == true {
model[indexpath.row].isCellSelected = false
}
else {
model[indexpath.row].isCellSelected = true
}
tableview.reloadData
}
And in cell for row check
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if model[index.row].isCellSelected{
cell.accessoryType = .checkmark
}
else {
cell.accessoryType = .none
}
}
What I did to make all work is:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let data = model[indexPath.row]
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .checkmark
model[indexPath.row].isCellSelected = true
self.selectedData?(model.filter{$0.isCellSelected})
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let data = model[indexPath.row]
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .none
model[indexPath.row].isCellSelected = false
self.selectedData?(model.filter{$0.isCellSelected})
}
}
and
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = model[indexPath.row]
let identifier = data.subtitle != nil ? kSubtitleID : kBasicID
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
cell.textLabel?.text = data.title
cell.detailTextLabel?.text = data.subtitle
cell.accessoryType = data.isCellSelected ? .checkmark : .none
return cell
}
but I had to initially setup which cells are selected actually:
func setSelectedCells(){
let selectedIndexes = self.model.enumerated().compactMap { (index, element) -> IndexPath? in
if element.isCellSelected{
return IndexPath(row: index, section: 0)
}
return nil
}
selectedIndexes.forEach{
selectRow(at: $0, animated: false, scrollPosition: .none)
}
}
and then called this in viewDidAppear, cause I had to be sure that table has drawn its content (cause this will fail if we try something (cell) that doesn't exist yet). Not a best way, but it solved the issue for single and multiple selections with initial states.
Then, cellForRowAt method becomes as simple as:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = model[indexPath.row]
let identifier = data.subtitle != nil ? kSubtitleID : kNormalID
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
cell.textLabel?.text = data.title
cell.detailTextLabel?.text = data.subtitle
cell.accessoryType = data.isCellSelected ? .checkmark : .none
return cell
}
Then, didSelectRowAt method becomes as simple as:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let cell = tableView.cellForRow(at: indexPath) {
let isCellSelected = !(model[indexPath.row].isCellSelected)
cell.accessoryType = isCellSelected ? .checkmark : .none
model[indexPath.row].isCellSelected = isCellSelected
}
}
Note we had to toggle the flag and update the cell and the model based on the new value.
Even better, you can replace the .accessory type updating line with:
tableView.reloadRows(at: [indexPath], with: .none)
So the method would look like this:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let cell = tableView.cellForRow(at: indexPath) {
let isCellSelected = !(model[indexPath.row].isCellSelected)
model[indexPath.row].isCellSelected = isCellSelected
tableView.reloadRows(at: [indexPath], with: .none)
}
}
Note that we update the model, then we reload the cell that will cause cellForRowAt to be called, and that method takes care on proper configurations based on model.
More generally, I'd recommend to use some StateController object to keep the state for each cell / row and take care of the toggling.
I've wrote a whole article a while ago that does exactly what you need to do, and showcases a lot of best practices too:
https://idevtv.com/how-to-display-a-list-of-items-in-ios-using-table-views/
#Whirlwind, Hi its not so complicate if you need to show selections only inside the tableView, here i am delivering my ans after your model update.
This can be done by maintaining isCellSelected property in didSelectRowAt only.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
model[indexPath.row].isCellSelected = model[indexPath.row].isCellSelected ? false : true
tableView.reloadData()
}
Here is cellForRowAt you can also alter some more lines here.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = model[indexPath.row]
let identifier = "kNormalID"
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
cell.textLabel?.text = data.title
cell.detailTextLabel?.text = data.subtitle
cell.accessoryType = data.isCellSelected ? .checkmark : .none
return cell
}
Hope i delivered you my best. Let me know if i missed anything.
Why don't you try following!
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .none
model[indexPath.row].isCellSelected = false
tableView.reloadRows(at: [indexPath], with: <UITableViewRowAnimation>)
}
}

UITableViewCell has incorrect change on indexPath

I try to use UITableViewCell's accessoryType property to checkmark cell when clicked ,but when cell selected , the checkmark set several time for different cells for example when I select row [0] , row [0] and row [8] and row [17] AccessoryType set to checkmark !
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "npCell", for: indexPath) as! NewPlaylistTableViewCell
cell.mTitle.text = musics[indexPath.row]["title"] as! String?
cell.mArtist.text = musics[indexPath.row]["artist"] as! String?
cell.selectionStyle = .none
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
cell?.accessoryType = .checkmark
tableView.reloadData()
}
For single selection, you need to track your selected indexPath in a viewController variable,
var selectedIndexPath : IndexPath?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.selectedIndexPath = indexPath
tableView.reloadData()
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "npCell", for: indexPath) as! NewPlaylistTableViewCell
cell.mTitle.text = musics[indexPath.row]["title"] as! String?
cell.mArtist.text = musics[indexPath.row]["artist"] as! String?
cell.accessoryType = .none
cell.selectionStyle = .none
if(indexPath == selectedIndexPath) {
cell.accessoryType = .checkmark
}
return cell
}
Even better (avoiding reload the entire UITableView)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let previousSelectedIndexPath = self.selectedIndexPath
self.selectedIndexPath = indexPath
if(previousSelectedIndexPath != nil) {
self.tableView.reloadRows(at: [previousSelectedIndexPath!,self.selectedIndexPath!], with: .automatic)
}else{
self.tableView.reloadRows(at: [self.selectedIndexPath!], with: .automatic)
}
self.tableView.reloadData()
}
UPDATE, Allowing multiple selection
For multiple selection you should track selected cells in a Dictionary for convenience faster access to selected and unselected indexPaths allowing you use multiple sections because the key value of our Dictionary is a string formed by (IndexPath.section)+(IndexPath.row) which is always unique combination
var selectedIndexPaths : [String:Bool] = [:]
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let currentIndexPathStr = "\(indexPath.section)\(indexPath.row)"
if(self.selectedIndexPaths[currentIndexPathStr] == nil || !self.selectedIndexPaths[currentIndexPathStr]!) {
self.selectedIndexPaths[currentIndexPathStr] = true
}else{
self.selectedIndexPaths[currentIndexPathStr] = false
}
self.tableView.reloadRows(at: [indexPath], with: .automatic)
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "npCell", for: indexPath) as! NewPlaylistTableViewCell
cell.mTitle.text = musics[indexPath.row]["title"] as! String?
cell.mArtist.text = musics[indexPath.row]["artist"] as! String?
cell.accessoryType = .checkmark
let currentIndexPathStr = "\(indexPath.section)\(indexPath.row)"
if(self.selectedIndexPaths[currentIndexPathStr] == nil || !self.selectedIndexPaths[currentIndexPathStr]!) {
cell.accessoryType = .none
}
return cell
}
Results

How to deselect a row when another row tapped?

Here is what I want to archive: deselect the "English" row when the second Chinese row tapped, or conversely.
I want only one checkmark to be shown, not all of them. Here is my code:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView.cellForRow(at: indexPath)!.accessoryType == .checkmark {
tableView.cellForRow(at: indexPath)!.accessoryType = .none
navigationItem.rightBarButtonItem!.isEnabled = false
} else if tableView.cellForRow(at: indexPath)!.accessoryType == .none {
tableView.cellForRow(at: indexPath)!.accessoryType = .checkmark
navigationItem.rightBarButtonItem!.isEnabled = true
}
}
Finally, I got an answer. Here you go:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)!.accessoryType = .checkmark
navigationItem.rightBarButtonItem?.isEnabled = true
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)!.accessoryType = .none
navigationItem.rightBarButtonItem?.isEnabled = false
}

Resources