I want to restrict certain sections of my tableview to only allow 1 cell to be selected, as of right now all my cells can be selected regardless of the section it's in
There is a little twist however : My sections are an [array] and change dynamically depending on different variables.
My sections are themselves each, a Variable, so I can pinpoint to them programmatically like this :
var section1 = [NSDictionary(objects: [NSLocalizedString("Alcohol use less than 24 hours", comment:""), 2],
EDIT2: It's been pointed out that I could create a var containing the restriction
var restrictedSections: [[NSDictionary]: Bool] {return [section1: true,section2: true,section3: true, section4: true, section4COPI: true, section5: true, section5COPI: true, section6: false, section7: false, section8: false] }
This cannot be referenced as an IndexPath, so no luck there... but maybe on the right path?
Some code from the tableView (simplified for StackOverflow readers) :
// checkmarks when tapped
func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
if self.selectedIndexPaths.contains(indexPath) {
cell.accessoryType = .none
cell.backgroundColor = UIColor.clear
self.selectedIndexPaths.remove(indexPath)
if CrewMembersNumber == "1" {
if((indexPath).section == 0) {
self.section1score -= section1[(indexPath).row].object(forKey: "value") as! Int
} else if((indexPath).section == 1) {
self.section3score -= section3[(indexPath).row].object(forKey: "value") as! Int
}
} else if CrewMembersNumber == "2" {
if((indexPath).section == 0) {
self.section1score -= section1[(indexPath).row].object(forKey: "value") as! Int
} else if((indexPath).section == 1) {
self.section2score -= section2[(indexPath).row].object(forKey: "value") as! Int
}
} else if CrewMembersNumber == "3" {
if((indexPath).section == 0) {
self.section1score -= section1[(indexPath).row].object(forKey: "value") as! Int
} else if((indexPath).section == 1) {
self.section5score -= section5[(indexPath).row].object(forKey: "value") as! Int
}
} else {
// if crewmemebernumber doest return 1-2 or 3
if((indexPath).section == 0) {
self.section1score -= section1[(indexPath).row].object(forKey: "value") as! Int
} else if((indexPath).section == 1) {
self.section4score -= section4[(indexPath).row].object(forKey: "value") as! Int
}
}
} else {
cell.accessoryType = .checkmark
cell.backgroundColor = UIColor (red:236/255.0, green: 236/255, blue: 236/255, alpha: 1.0)
self.selectedIndexPaths.add(indexPath)
if CrewMembersNumber == "1" {
if((indexPath).section == 0) {
self.section1score += section1[(indexPath).row].object(forKey: "value") as! Int
} else if((indexPath).section == 1) {
self.section3score += section3[(indexPath).row].object(forKey: "value") as! Int
}
} else if CrewMembersNumber == "2" {
if((indexPath).section == 0) {
self.section1score += section1[(indexPath).row].object(forKey: "value") as! Int
} else if((indexPath).section == 1) {
self.section2score += section2[(indexPath).row].object(forKey: "value") as! Int
}
} else if CrewMembersNumber == "3" {
if((indexPath).section == 0) {
self.section1score += section1[(indexPath).row].object(forKey: "value") as! Int
} else if((indexPath).section == 1) {
self.section5score += section5[(indexPath).row].object(forKey: "value") as! Int
}
} else {
// if crewmemebernumber doest return 1-2 or 3
if((indexPath).section == 0) {
self.section1score += section1[(indexPath).row].object(forKey: "value") as! Int
} else if((indexPath).section == 1) {
self.section4score += section4[(indexPath).row].object(forKey: "value") as! Int
}
}
}
self.updateToolbarAndLabel(self.totalScore)
self.tableView.reloadData()
}
}
func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel!.text = self.textForIndexPath(indexPath);
cell.textLabel!.font = UIFont(name:"Avenir", size:19)
cell.textLabel!.numberOfLines = 0
cell.selectionStyle = UITableViewCellSelectionStyle.none;
if(self.selectedIndexPaths.contains(indexPath)) {
cell.accessoryType = .checkmark;
cell.backgroundColor = UIColor (red:236/255.0, green: 236/255, blue: 236/255, alpha: 1.0)
} else {
cell.accessoryType = .none;
cell.backgroundColor = UIColor.clear
}
return cell
}
As you can see when I tap a cell it adds a value and the cell changes it's backgroundColor And adds a checkmark,
What I need to do is if there is a cell that is selected, in certain sections that only 1 can be selected, it needs to check if any cell within that section is selected and deselect it in favor of the new one the user tapped on. Right now I do not understand how to do this at all
thanks for any help
Fundimentally the best solution (IMHO) for tableviews is to create a view model for your table, manipulate the data as required, then reflect that data in the table. Then, you do everything possible to have the table react to data changes as opposed to trying to use the table view itself to reflect data or state.
EDIT: instead of using reloadData
, the code now uses performBatchUpdates
for a more elegant presentation.
I created a project that does what you want and you can find it here.
The view data is contained here:
let pilots = "Pilots"
let crew = "Crew"
let passengers = "Passengers"
var sections: [String] = []
var multipleSelectionsAllowed: Set<String> = []
var members: [String: [String]] = [:]
var selectedMembers: Set<String> = []
the first three string constants allow us to index into the data, and initialized:
sections = [pilots, crew, passengers] // initial ordering of sections
multipleSelectionsAllowed = [passengers]
The data is created programmatically, see the attached project or the full code attached below.
You said the sections may change, so sections
is a variable and we'll change it later on.
selectedMembers
contains a hash
of the type
(i.e. Pilot, Crew, or Passenger and their name, so it should be unique. This array will reflect the current selections, as data and not indexPaths.
But, we need indexPaths to reflect the isSelected
UI changes: fine, we'll use two functions for this:
typealias KeyToValues = (section: String, name: String)
func sectionNameToHash(section: String, name: String) -> String {
let hash = section + "|" + name
return hash
}
func hashToSectionName(hash: String) -> KeyToValues {
let array = hash.components(separatedBy: "|")
assert(array.count == 2)
return (array[0], array[1])
}
Also, something I've found very useful in the past is to put the code that changes the look of a cell in a single place, and call it when a cell is created or changed. You won't get out of sync over time as the UI changes too.
func updateCell(atIndexPath indexPath: IndexPath) {
let cells = tableView.visibleCells
for cell in cells {
guard let path = tableView.indexPath(for: cell) else { continue }
if path == indexPath {
updateCell(cell, atIndexPath: indexPath)
}
}
}
func updateCell(_ cell: UITableViewCell, atIndexPath indexPath: IndexPath) {
let section = sections[indexPath.section]
guard let names = members[section] else { fatalError() }
let name = names[indexPath.row]
let hash = sectionNameToHash(section: section, name: name)
let shouldBeSelected = selectedMembers.contains(hash)
if shouldBeSelected {
cell.accessoryType = .checkmark
print("SELECTED", hash)
} else {
cell.accessoryType = .none
print("DESELECTED", hash)
}
}
You need both because in some cases you only have an indexPath, not the cell.
Note that you use the above methods when creating cells:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let section = sections[indexPath.section]
guard let names = members[section] else { fatalError() }
let name = names[indexPath.row]
cell.textLabel?.text = name
updateCell(cell, atIndexPath: indexPath)
return cell
}
When the tableView detects a selection, you will first look at the existing selected data, and first remove that selection from your data, then update any delected cell's UI:
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
let section = sections[indexPath.section]
guard let names = members[section] else { fatalError() }
let canMultipleSelect = multipleSelectionsAllowed.contains(section)
if !canMultipleSelect, let paths = tableView.indexPathsForSelectedRows {
for path in paths {
if path.section == indexPath.section {
let name = names[path.row]
let hash = sectionNameToHash(section: section, name: name)
selectedMembers.remove(hash)
updateCell(atIndexPath: path)
tableView.deselectRow(at: path, animated: true)
}
}
}
return indexPath
}
Then, handle the selection method:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let section = sections[indexPath.section]
guard let names = members[section] else { fatalError() }
let name = names[indexPath.row]
let hash = sectionNameToHash(section: section, name: name)
selectedMembers.insert(hash)
print("SELECTED THE CELL AT", hash)
updateCell(atIndexPath: indexPath)
}
Voila - everything works as you want. But, even better, you can re-arrange the sections as you said you do and get everything properly selected. The example code re-arranges the sections 5 seconds after you select the first row/column
if indexPath.section == 0 && indexPath.row == 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.sections = [self.crew, self.pilots, self.passengers] // changed!
tableView.reloadData()
// all selections from the tableView are now gone
// NOTE: none of the other data changes!
for hash in self.selectedMembers {
let value = self.hashToSectionName(hash: hash)
guard
let sectionNumber = self.sections.firstIndex(of: value.section),
let names = self.members[value.section],
let row = names.firstIndex(of: value.name)
else { fatalError() }
let indexPath = IndexPath(row: row, section: sectionNumber)
self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
}
}
}
The reload() erases all selections, so the above code uses the known selected members to inform the tableView of list of selections, even if the cells for each are not visible.
The complete class
import UIKit
private final class MyCell: UITableViewCell {
override var reuseIdentifier: String? { "cell" }
}
final class ViewController: UITableViewController {
let pilots = "Pilots"
let crew = "Crew"
let passengers = "Passengers"
var sections: [String] = []
var multipleSelectionsAllowed: Set<String> = []
var members: [String: [String]] = [:]
var selectedMembers: Set<String> = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(MyCell.self, forCellReuseIdentifier: "cell")
tableView.allowsMultipleSelection = true
sections = [pilots, crew, passengers] // initial ordering of sections
multipleSelectionsAllowed = [passengers]
constructData()
}
private func constructData() {
var array: [String] = []
(1..<6).forEach { array.append("Pilot \($0)")}
members[pilots] = array
array.removeAll()
(1..<20).forEach { array.append("Crew \($0)")}
members[crew] = array
array.removeAll()
(1..<250).forEach { array.append("Passenger \($0)")}
members[passengers] = array
}
// MARK: - Helpers -
typealias KeyToValues = (section: String, name: String)
func sectionNameToHash(section: String, name: String) -> String {
let hash = section + "|" + name
return hash
}
func hashToSectionName(hash: String) -> KeyToValues {
let array = hash.components(separatedBy: "|")
assert(array.count == 2)
return (array[0], array[1])
}
}
extension ViewController /*: UITableViewDataSource */ {
override func numberOfSections(in: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let type = sections[section]
let count = members[type]?.count ?? 0 // could use guard here too and crash if nil
return count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let section = sections[indexPath.section]
guard let names = members[section] else { fatalError() }
let name = names[indexPath.row]
cell.textLabel?.text = name
updateCell(cell, atIndexPath: indexPath)
return cell
}
func updateCell(atIndexPath indexPath: IndexPath) {
let cells = tableView.visibleCells
for cell in cells {
guard let path = tableView.indexPath(for: cell) else { continue }
if path == indexPath {
updateCell(cell, atIndexPath: indexPath)
}
}
}
func updateCell(_ cell: UITableViewCell, atIndexPath indexPath: IndexPath) {
let section = sections[indexPath.section]
guard let names = members[section] else { fatalError() }
let name = names[indexPath.row]
let hash = sectionNameToHash(section: section, name: name)
let shouldBeSelected = selectedMembers.contains(hash)
if shouldBeSelected {
cell.accessoryType = .checkmark
print("SELECTED", hash)
} else {
cell.accessoryType = .none
print("DESELECTED", hash)
}
}
}
extension ViewController /* : UITableViewDelegate */ {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
let section = sections[indexPath.section]
guard let names = members[section] else { fatalError() }
let canMultipleSelect = multipleSelectionsAllowed.contains(section)
if !canMultipleSelect, let paths = tableView.indexPathsForSelectedRows {
for path in paths {
if path.section == indexPath.section {
let name = names[path.row]
let hash = sectionNameToHash(section: section, name: name)
selectedMembers.remove(hash)
updateCell(atIndexPath: path)
tableView.deselectRow(at: path, animated: true)
}
}
}
return indexPath
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let section = sections[indexPath.section]
guard let names = members[section] else { fatalError() }
let name = names[indexPath.row]
let hash = sectionNameToHash(section: section, name: name)
selectedMembers.insert(hash)
print("SELECTED THE CELL AT", hash)
updateCell(atIndexPath: indexPath)
if indexPath.section == 0 && indexPath.row == 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.sections = [self.crew, self.pilots, self.passengers]
tableView.reloadData()
// all selections from the tableView are gone
for hash in self.selectedMembers {
let value = self.hashToSectionName(hash: hash)
guard
let sectionNumber = self.sections.firstIndex(of: value.section),
let names = self.members[value.section],
let row = names.firstIndex(of: value.name)
else { fatalError() }
let indexPath = IndexPath(row: row, section: sectionNumber)
self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
}
}
}
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
print("DESELECTED THE CELL AT", hash)
let section = sections[indexPath.section]
guard let names = members[section] else { fatalError() }
let name = names[indexPath.row]
let hash = sectionNameToHash(section: section, name: name)
selectedMembers.remove(hash)
updateCell(atIndexPath: indexPath)
}
}