Right now I can add checkmarks to the table of choice selected using your approach vadian. I also added the Boolean("selected") in my data class. What I dont get is how to save the boolean selected for each row with UserDefaults or than how to load this in cell for row at table. Do I need to even touch didload section?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? TableCell else {
return UITableViewCell()
}
let product = products[indexPath.row]
cell.accessoryType = product.selected ? .checkmark : .none
return cell }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? TableCell else {
return UITableViewCell()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
var selected = products[indexPath.row].selected
products[indexPath.row].selected = !selected
tableView.reloadRows(at: [indexPath], with: .none)
if selected != true {print("selected", indexPath.row, selected )}
else {print("selected", indexPath.row, selected )}
selected = UserDefaults.standard.bool(forKey: "sound")
}
Add a property var approved : Bool to your data model
Update the property in the model when the selection changes and reload the row.
In cellForRow set the checkmark depending on the property
...
let cell = tableview.dequeueReusableCell(withIdentifier: "myfeedTVC", for: indexPath) as! MyFeedTVC
let user = userDataArray[indexPath.row]
cell.accessoryType = user.approved ? .checkmark : .none
...
Related
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()
}
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>)
}
}
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
I'm pretty new to Swift and I'm working on my first app. I'm currently using the UITableView that has an option for checkmarks to appear on the right when users tap on it. It works fine but whenever you scroll down on the list of items, the checkmarks disappear. I've checked a few online sources but I'm unsure of how to apply it to the code I have. Any help would be greatly appreciated!
Also, is there any way that I can store the checkmarks for when the user reopens the app? Every time I restart the app, the list of checks resets.
Here is the code I have so far:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "celltwo")
cell.textLabel?.text = list[indexPath.row]
return(cell)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView.cellForRow(at: indexPath)?.accessoryType == UITableViewCellAccessoryType.checkmark
{
tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.none
}
else
{
tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.checkmark
}
tableView.deselectRow(at: indexPath, animated: true)
}
The checkmarks are disappearing on scrolling because table views reuse the cells so the 'cellForRowAt' method gets called whenever you scroll and you haven't provided the logic to show/hide the checkmark in this method. To solve this you can do the following,
Initialise an Array to store the indexes of the selected cells.
var selectedIndexes : [Int] = []
Update your 'didSelectRowAt' method with the logic to add/remove indexes to/from the 'selectedIndexes' array.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView.cellForRow(at: indexPath)?.accessoryType == UITableViewCellAccessoryType.checkmark
{
tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.none
let indexOfItemToRemove = self.selectedIndexes.index(of: list[indexPath.row])
self.selectedIndexes.remove(at: indexOfItemToRemove)
}
else
{
tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.checkmark
self.selectedIndexes.append(indexPath.row)
}
}
Update your 'cellForRowAt' method with the logic to show/hide checkmark.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "celltwo")
cell.textLabel?.text = list[indexPath.row]
if self.selectedIndexes.contains(indexPath.row)
{
//cell was selected earlier
cell.accessoryType = UITableViewCellAccessoryType.checkmark
}else
{
// cell was not selected earlier
cell.accessoryType = UITableViewCellAccessoryType.none
}
return cell
}
In order to save the selection for the next time the app is launched you could save the 'selectedIndexes' array to UserDefaults. In order to achieve this do the following :
Update the 'didSelectRowAt' method to include the logic to save the selected index to UserDefaults. At the following code at the end of the method.
let userDefaults = UserDefaults.standard
userDefaults.set(selectedIndexes, forKey: "SelectedIndexes")
Add the following code to the 'viewDidLoad' method.
let userDefaults = UserDefaults.standard
self.selectedIndexes = userDefaults.value(forKey: "SelectedIndexes")
The cell accessoryType disappears because the reusability feature the UITableView use, in order to keep the selection follow the following code:
override func viewDidLoad()
{
super.viewDidLoad()
tableView.allowsMultipleSelection = true
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "celltwo")
cell.textLabel?.text = list[indexPath.row]
if let paths = tableView.indexPathsForSelectedRows
{
if (paths.contains(indexPath))
{
cell.accessoryType = .checkmark
}
else
{
cell.accessoryType = .none
}
}
else
{
cell.accessoryType = .none
}
cell.selectionStyle = .none
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
if tableView.cellForRow(at: indexPath)?.accessoryType == UITableViewCellAccessoryType.checkmark
{
tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.none
}
else
{
tableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.checkmark
}
//remove deselection
//tableView.deselectRow(at: indexPath, animated: true)
}
Regard saving the selection, this info I guess should be persisted with your datasource in CoreData or UserDefaults.
I keep getting checkmarks being marked in other sections of my table view when I click a row. Im not certain if I need to set my accessoryType. I tried mytableView.reloadData() however that didn't help either.
var selected = [String]()
var userList = [Users]()
#IBOutlet weak var myTableView: UITableView!
#IBAction func createGroup(_ sender: Any) {
for username in self.selected{
ref?.child("Group").childByAutoId().setValue(username)
print(username)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
myCell.selectionStyle = UITableViewCellSelectionStyle.none
myCell.nameLabel.text = userList[indexPath.row].name
return myCell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if myTableView.cellForRow(at: indexPath)?.accessoryType == UITableViewCellAccessoryType.checkmark{
myTableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.none
let currentUser = userList[indexPath.row]
selected = selected.filter { $0 != currentUser.name}
}
else{
myTableView.cellForRow(at: indexPath)?.accessoryType = UITableViewCellAccessoryType.checkmark
let currentUser = userList[indexPath.row]
selected.append(currentUser.name!)
}
}
Your problem is not inside this method but in the one that "loads" the cells. (cell for row)
Since Table Views use reusable cells, more often than not they will be loading a cell that was already presented somewhere else.
Because of this, on the cell loading method you should "Reset the state" the loaded cell, this includes the accessory type, and any other properties you might have changed.
So just change this in your code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
myCell.selectionStyle = UITableViewCellSelectionStyle.none
myCell.nameLabel.text = userList[indexPath.row].name
// ADD THIS
if userList[indexPath.row].isSelected {
myCell.accessoryType = UITableViewCellAccessoryType.checkmark
} else {
myCell.accessoryType = UITableViewCellAccessoryType.none
}
return myCell
}
EDIT:
"userList[indexPath.row].isSelected" is a property that YOU have to create and manage. (So you must also modify it in the didSelectRowAt method.
The issue is you are not maintaining the selected User info properly, which will be used while you scroll the table and when the cell has to reload data.
As you have already created var selected = [String]() , I suggest using the same.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
let dataFoundInselectedArr = selected.filter { $0 == userList[indexPath.row].name}
if(dataFoundInselectedArr.count > 0){
myCell.accessoryType = UITableViewCellSelectionStyle.checkmark
}else{
myCell.accessoryType = UITableViewCellSelectionStyle.none
}
myCell.nameLabel.text = userList[indexPath.row].name
return myCell
}
The table selection delegate method remains the same.