I am trying to make a search bar in iOS and have made it to where it filters results and then when you click on it, it shows a checkmark. When I delete the search text, the check mark goes away and the full page of cells appears but the cell that I selected in the search is not selected with a check mark. This is implemented in my cell by setSelected method:
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected {
accessoryType = .checkmark
} else {
accessoryType = .none
}
}
I haven't any logic with checkmark in cellForRowAt and didSelectRowAt methods.
Here is a GIF of my problem
Can someone help me?
I assume all you're currently doing is setting the checkmark on your cell. The cell will be reused and you'll lose the checkmark.
What you need to do is to keep track of the items, e.g. in a Set, that the user has checked so far. Upon rendering items in the table view, you'll need to consult this set whether the item is checked. If it is, you'll want to add the checkmark to the cell.
Something along these lines where Item is your cell model. This should get you started. You should be able to extend this to a data set with groups for table headers.
/// The items that have been checked.
var checkedItems = Set<Item>()
/// All items that are shown when no search has been performed.
var allItems: [Item]
/// The currently displayed items, which is the same as `allItems` in case no
/// search has been performed or a subset of `allItems` in case a search is
/// currently active.
var displayedItems: [Item]
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = displayedItems[indexPath.row]
let cell = // Construct your cell as usual.
if checkedItems.contains(item) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let item = displayedItems[indexPath.row]
if checkedItems.contains(item) {
checkedItems.remove(item)
cell.accessoryType = .none
} else {
checkedItems.insert(item)
cell.accessoryType = .checkmark
}
}
func updateSearchResults(for searchController: UISearchController) {
displayedItems = // Set the displayed items based on the search text.
tableView.scrollRectToVisible(.zero, animated: false)
tableView.reloadData()
}
I solved my problem in this way:
var selectedCellIndex: String?
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let key = sectionTitles[indexPath.section]
if let values = dictionary[key] {
selectedCellIndex = values[indexPath.row]
}
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
}
public func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = .none
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.tintColor = Color.main()
cell.selectionStyle = .none
let key = sectionTitles[indexPath.section]
if let values = dictionary[key] {
cell.textLabel?.text = values[indexPath.row]
if values[indexPath.row] == selectedCellIndex {
cell.accessoryType = .checkmark
}
}
return cell
}
Related
I have a weird situation where I swipe a cell to grey it out and it greys every 4th or 6th cell instead of only the single cell that was swiped.
The tableview is initialized as follows:
func setupView() {
view.backgroundColor = .white
tableView.register(EntityCell.self, forCellReuseIdentifier: "entityCell")
tableView.separatorStyle = .none
tableView.dataSource = self
tableView.delegate = self
}
Here is my query to get the data:
func getEntities(taxId : String) {
dispatchGroup.enter()
db.collection("Taxonomy").whereField("entityId", isEqualTo: entityId).whereField("status", isEqualTo: 401).getDocuments { (orderSnapshot, orderError) in
if orderError != nil {
self.showError(show: "Error", display: orderError!.localizedDescription)
} else {
self.entitiesArray.append(contentsOf: (orderSnapshot?.documents.compactMap({ (orderDocuments) -> Order in
Order(dictionary: orderDocuments.data(), invoiceId: orderDocuments.documentID, opened: false)!
}))!)
self.dispatchGroup.leave()
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
Here are the standard override functions to populate the tableview:
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return entitiesArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "entityCell", for: indexPath) as? EntityCell else { return UITableViewCell() }
let entityRow = entitiesArray[indexPath.row]
cell.selectionStyle = .none
cell.setTaxonomy(entity: entityRow) // Setting up the cell with the array values
return cell
}
Everything is working fine upto this point. And finally here is the override func for swipe action:
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let complete = UIContextualAction(style: .normal, title: "Verified") { (action, view, completionHandler) in
self.db.collection("Taxonomy").document(self.entitiesArray[indexPath.row].entityId).updateData(["status": 411]) { (error) in
if error == nil {
let cell = tableView.cellForRow(at: indexPath) as? EntityCell
cell?.changeStatus(currentEntity: self.entitiesArray[indexPath.row])
}
}
completionHandler(true)
}
complete.image = UIImage(named: "icon_approved")
complete.backgroundColor = UIColor(hex: Constants.Colors.secondary)
let swipe = UISwipeActionsConfiguration(actions: [complete])
return swipe
}
So I swipe right from the trailing edge of the cell and I see the underlying color and icon as expected. And the cell turns grey via this function via a protocol:
extension EntityCell : EntityStatusDelegate {
func changeStatus(currentEntity: EntityObject) {
entityCellBackground.backgroundColor = .systemGray4
}
}
The cell turns grey. And then I scroll down and I see every 4th or 6th cell is grey as well. Any idea what is going wrong? I am pretty flummoxed at this point.
Cells get recycled. You need either configure them completely or overwrite the prepareForReuse function of the cell or give each cell an unique reuseidentifyer so the tableview can recycle them.
(Last option is the worst as it cost a lot more memory)
Option 1:
Just set the backgroundcolor:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "entityCell", for: indexPath) as? EntityCell else { return UITableViewCell() }
let entityRow = entitiesArray[indexPath.row]
cell.selectionStyle = .none
cell.setTaxonomy(entity: entityRow) // Setting up the cell with the array values
cell.entityCellBackground.backgroundColor = (whatever the default color is)
return cell
}
It's easier to explain by example. I have original array which is searched and filtered array with searched items. If i found one item after searching and tap on it, i mark it as done (I have todo list), but when i cancel my search, I find that the first element in the original array is marked, not the third item.
I googled some threads and found almost similar problems, but solutions doesn't suit to my problem. For example:
didSelectRowAtIndexPath indexpath after filter UISearchController - Swift
And here some code. Especially at didSelectRowAt I mark the items to done. Does anyone have any ideas?
private var searchBarIsEmpty: Bool {
guard let text = searchController.searchBar.text else { return false }
return text.isEmpty
}
private var isFiltering: Bool {
return searchController.isActive && !searchBarIsEmpty
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering {
return filteredTasks?.count ?? 0
}
return manager.tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Keys.cell.rawValue, for: indexPath) as! ToDoCell
var currentItem: Task
if isFiltering {
currentItem = filteredTasks?[indexPath.row] ?? manager.tasks[indexPath.row]
} else {
currentItem = manager.tasks[indexPath.row]
}
cell.titleLabel.text = currentItem.taskName
cell.descriptionLabel.text = currentItem.description
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let accessoryType: UITableViewCell.AccessoryType = manager.changeState(at: indexPath.row) ? .checkmark : .none
tableView.cellForRow(at: indexPath)?.accessoryType = accessoryType
}
When you use tableView.dequeueReusableCell, you may get the old cell, so you should update it. You should read doc.
#PGDev already said in comments that you should save checked/unchecked status in your model.
I hope my example will help you.
You can contain state of cells in cell models:
class YourCellModel {
var task: Task
var checked: Bool
init(task: Task, checked: Bool) {
self.task = task
self.checked = checked
}
}
And add it in ToDoCell:
//...
var model: YourCellModel {
didSet {
updateViews()
}
}
func updateViews() {
titleLabel.text = task.taskName
descriptionLabel.text = task.description
if model.checked {
//...
} else {
//....
}
}
And update model here:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Keys.cell.rawValue, for: indexPath) as! ToDoCell
// You should contain cell models to remember their states
let model = cellModels[indexPath.row]
cell.model = model
return cell
}
When the user checks a cell, you should save it in your cell model. You can do it in ToDoCell:
func checked() {
model.checked = true
}
Note: If isFiltering is true, it is a different array of cell models.
UPD. I noticed your Task is similar to a cell model. You can save checked status there. But your cell should have access to it.
I am creating an application where when a User searches for an item in the TableView they can click on it and a checkmark appears next to it. However, say when I select the first item I have searched for and click it then delete my search the checkmark stays on the first row but for a completely different object, I searched for, to begin with (see images below).
When Searching
When not Searching
var searchingArray = [Symptoms]()
var filteredArray = [Symptoms]()
var selectedSymptoms = [Symptoms]()
var clicked = [String]()
var searchingUnderWay = false
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = searchingSymptomsTableView.dequeueReusableCell(withIdentifier: "ExtraSymptoms", for: indexPath) as? ExtraSymptomCell {
let searchingArrays: Symptoms!
if searchingUnderWay {
searchingArrays = self.filteredArray[indexPath.row]
} else {
searchingArrays = self.searchingArray[indexPath.row]
}
cell.updateUI(symptomNames: searchingArrays)
return cell
} else {
return UITableViewCell()
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedRow: Symptoms!
let symptomName: String!
let cell : UITableViewCell = tableView.cellForRow(at: indexPath)!
if searchingUnderWay {
selectedRow = filteredArray[indexPath.row]
symptomName = filteredArray[indexPath.row].name as String
if clicked.contains(symptomName) {
cell.accessoryType = .none
let indexNumber = clicked.index(of: symptomName)
clicked.remove(at: indexNumber!)
if let element = selectedSymptoms.index(where: { $0.name == selectedRow.name }) {
selectedSymptoms.remove(at: element)
}
} else {
clicked.append(symptomName)
cell.accessoryType = .checkmark
searchingSymptomsTableView.reloadData()
selectedSymptoms.append(selectedRow)
}
} else {
selectedRow = searchingArray[indexPath.row]
symptomName = searchingArray[indexPath.row].name as String
if clicked.contains(symptomName) {
cell.accessoryType = .none
let indexNumber = clicked.index(of: symptomName)
clicked.remove(at: indexNumber!)
if let element = selectedSymptoms.index(where: { $0.name == selectedRow.name }) {
selectedSymptoms.remove(at: element)
}
} else {
clicked.append(symptomName)
cell.accessoryType = .checkmark
searchingSymptomsTableView.reloadData()
selectedSymptoms.append(selectedRow)
}
print(clicked)
print(selectedSymptoms)
}
}
I wish for the item I searched using the searchbar to still be checked when you delete the search.
Many thanks
Welcome to TableViewController logic. It seems really strange, but it works correct)
You need to override prepareForReuse() method in your ExtraSymptomCell. And clear all the values your cell contains including accessoryType
override func prepareForReuse() {
super.prepareForReuse()
accessoryType = .none
}
In your tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath):
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = searchingSymptomsTableView.dequeueReusableCell(withIdentifier: "ExtraSymptoms", for: indexPath) as? ExtraSymptomCell {
let symptomName: String!
let searchingArrays: Symptoms!
if searchingUnderWay {
searchingArrays = self.filteredArray[indexPath.row]
symptomName = filteredArray[indexPath.row].name as String
} else {
searchingArrays = self.searchingArray[indexPath.row]
symptomName = filteredArray[indexPath.row] as String
}
cell.updateUI(symptomNames: searchingArrays)
if clicked.contains(symptomName) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
} else {
return UITableViewCell()
}
}
Since UITableViewCell are reused, the checkmark will appear in a cell when you reload Table Data.
In cellForRowAt set the accessoryType to .none, to remove a previously checked cell checkmark:
let cell : UITableViewCell = tableView.cellForRow(at: indexPath)!
cell.accessoryType = .none
This will remove the check from the previous search.
Im using a tableview to display an array of strings. When I click on a particular row, I want it to be highlighted with a checkmark. When I deselect a row, I want the checkmark to be removed. When I press a button, I want the rows that are currently highlighted to be passed out in an array(newFruitList).
My problem is that when I click the first row, the last is highlighted. When I uncheck the first row, the last is unchecked, as if they are the same cell?
How do I overcome this?
Also, the way I am adding and removing from my new array, is this the correct way to go about doing this?
Thanks
My Code:
class BookingViewController: UIViewController, ARSKViewDelegate, UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var table: UITableView!
let fruits = ["Apples", "Oranges", "Grapes", "Watermelon", "Peaches"]
var newFruitList:[String] = []
override func viewDidLoad() {
super.viewDidLoad()
self.table.dataSource = self
self.table.delegate = self
self.table.allowsMultipleSelection = true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fruits.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = table.dequeueReusableCell(withIdentifier: "Cell")
if cell == nil{
cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
}
cell?.textLabel?.text = fruits[indexPath.row]
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
newFruitList.append(fruits[indexPath.row])
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .checkmark
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let index = newFruitList.index(of: fruits[indexPath.row]) {
newFruitList.remove(at: index)
}
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .none
}
}
#IBAction func bookButtonPressed(_ sender: UIButton) {
//testing purposes
for i in stride(from: 0, to: newFruitList.count, by: 1){
print(newFruitList[i])
}
}
They are probably the same cell because you use dequeueReusableCell and it reuses old cells.
use:
override func prepareForReuse() {
super.prepareForReuse()
}
To reset the cell and it should be fine.
As for the save and send mission. Create an pre-indexed array that you can populate.
var selected: [Bool] = []
var fruits: [Fruit] = [] {
didSet {
selected = Array(repeating: false, count: fruits.count)
}
}
And in your didSelectItemAt you do:
selected[indexPath.item] = !selected[indexPath.item]
UITableView reuses the cell that is already present and hence you will see that duplicate check mark, so to solve this issue you need to clear the cell states while loading cell. for that you can create a model with property to track the states of your selected cells
So your fruit model must be like below
class Fruit{
var name:String
var isSelected:Bool
init(name:String){
isSelected = false
self.name = name
}
}
Then you will have table view populated with Fruit list
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = table.dequeueReusableCell(withIdentifier: "Cell")
if cell == nil{
cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
}
let model = fruits[indexPath.row]
cell?.textLabel?.text = model.name
if(model.isSelected){
cell.accessoryType = .checkmark
}
else{
cell.accessoryType = .none
}
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
newFruitList.append(fruits[indexPath.row])
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .checkmark
var model = fruits[indexPath.row]
model.isSelected = true
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let index = newFruitList.index(of: fruits[indexPath.row]) {
newFruitList.remove(at: index)
}
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .none
var model = fruits[indexPath.row]
model.isSelected = false
}
}
I have a multi selected tableview.
what I am doing : when user select items, this items append to the array.
When user deselect the item from cell, this deselected items remove from array.
what I did :
My array : var selectedTagList:[Tag] = []
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tagTableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
self.selectedTagList.append(tagList![indexPath.row])
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
self.tagTableView.cellForRow(at: indexPath)?.accessoryType = .none
self.selectedTagList.remove(at: indexPath.row)
}
Any advice or sample code please ?
//DataSource and Delegate
extension PickVideoViewController : UITableViewDataSource,UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let tlist = self.tagList , !tlist.isEmpty else { return 1}
return tlist.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let tlist = self.tagList , !tlist.isEmpty else {
let cell = UITableViewCell()
cell.selectionStyle = .none
cell.backgroundColor = .clear
cell.textLabel?.textAlignment = .center
cell.textLabel?.textColor = UIColor.black
cell.textLabel?.text = "nodataavaiable".localized()
return cell }
let cell = tableView.dequeueReusableCell(withIdentifier: "TagCell", for: indexPath) as! TagCell
cell.tagName.text = tlist[indexPath.row].tag
cell.accessoryType = cell.isSelected ? .checkmark : .none
cell.selectionStyle = .none // to prevent cells from being "highlighted"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tagTableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
self.selectedTagList.append(tagList![indexPath.row])
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
self.tagTableView.cellForRow(at: indexPath)?.accessoryType = .none
self.selectedTagList.remove(at: <#T##Int#>)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let tlist = self.tagList , !tlist.isEmpty else {return tableView.frame.height }
return 40
}
}
self.selectedTagList.remove(at: indexPath.row)
indexPath.row is the wrong value to remove. Since your data source populates based on tagList and not selectedTagList, you need to get the item out of tagList and find the equivalent item in selectedTagList.
You don't show what type of object is in tagList, but you will probably need to make them conform to Equatable so you can do this lookup. Once you do, you should have something like this:
let deselectedTag = self.tagList[indexPath.row]
// You will need the items in `tagList` to conform to `Equatable` to do this
guard let indexOfDeselectedTag = self.selectedTagList.index(of: deselectedTag else) {
// Data inconsistency: the item wasn't found in selectedTagList
return
}
self.selectedTagList.remove(at: indexOfDeselectedTag)
You don't need to maintain a list of selected items. You already have all of the items and the tableView can tell you which rows/items are selected.
https://developer.apple.com/documentation/uikit/uitableview/1614864-indexpathsforselectedrows
Discussion
The value of this property is an array of index-path
objects each identifying a row through its section and row index. The
value of this property is nil if there are no selected rows.
You are trying to reinvent the wheel. Always check the documentation for existing functionality.
If you then want a list of selected items from this you just create an array of the items at those index paths, something like this: (untested)
let selectedItems = tableView.indexPathsForSelectedRows().map {
self.tagList[$0.row]
}
This code will iterate over the index paths and return the item from the array at each one. (this is untested, you may need to use flatMap as you are changing the type)