UISearchController: indexPath doesn't change with original array - swift

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.

Related

Swiping a UITableViewCell action affects every 4th cell in the table view instead of ONLY the target cell

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
}

Get textLabel from UITableView

I need to get textLabel from row in UITableView. But I get nil. I need to get textLabel, because I'm using UISearchBar, if I will try to get data using index, when searching, I will receive incorrect indexes. So , I want to get textLabel.
Please, fix where I'm wrong
When I am typing in searchBar items in TableView change. But index doesn't. For example Food = ["Apple", "Banana", "Coca-Cola"]. If I use searchBar and enter "Banana". Then I click on this , but I get Apple (because this is index - 0). That's why I want to get textLabel
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//let meal = foods[indexPath.item]
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let labelContent = cell.textLabel!.text
print(labelContent)
}
UISearchBar
extension FoodViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
searchedFoods = foods.filter({ $0.title.lowercased().prefix(searchText.count) == searchText.lowercased() })
searching = true
tableView.reloadData()
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searching = false
searchBar.text = ""
tableView.reloadData()
}
}
Never get data from the view, the cell, get it always from the model, the data source.
And never use dequeueReusableCell outside of cellForRowAt. You won't get the cell you expect.
In this case you have to get the data depending on searching
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let labelContent : String
if searching {
labelContent = searchedFoods[indexPath.row].title
} else {
labelContent = foods[indexPath.row].title
}
print(labelContent)
}
And your filter method is horrible. Change it to much more efficient
searchedFoods = foods.filter { $0.title.range(of: searchText, options: [.caseInsensitive, .anchored] != nil) }
Finally I recommend to change textDidChange to cancel searching also if the search text becomes empty and remove the unused items from searchedFoods.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty {
searchedFoods.removeAll()
searching = false
} else {
searchedFoods = foods.filter{ $0.title.range(of: searchText, options: [.caseInsensitive, .anchored]) != nil }
searching = true
}
tableView.reloadData()
}
As I have figured out the problem is that you are using indexPath.item that's why you are getting the wrong index please try this
let meal = foods[indexPath.row]
If you want to get a text from the UILabel on tap of cell
confirm UITableViewDelegate
//Access the array that you have used to fill the tableViewCell
print(foods[indexPath.row]) it will print selected row
OR
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! UITableViewCell
let labelContent = cell.textLabel!.text
print(labelContent)
}
Lets just say instead of getting data from foods (in didSelect method) get it from searched Food. But for that to work properly you should assign searched food with food initially and also when the user clears search bar you should again reset searched food to food data.
Please try like this,
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if searching {
print(searchedFoods[indexPath.row])
} else {
print(foods[indexPath.row])
}
}
Try This
extension FoodViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if searching {
let food = searchedFoods[indexPath.row]
cell.textLabel?.text = food.title
} else {
let food = foods[indexPath.row]
cell.textLabel?.text = food.title
}
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching {
return searchedFoods.count
} else {
return foods.count
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if searching {
let meal = searchedFoods[indexPath.row]
let labelContent = meal
print(labelContent)
} else {
let meal = foods[indexPath.row]
let labelContent = meal
print(labelContent)
}
}

How to save checkmark after using search in SearchBar?

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
}

Checkmark's associated with wrong row in TableView when using SearchBar

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.

Preventing last cell row being checked after clicking first and vice versa & adding/removing selected rows to array

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
}
}