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

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.

Related

UISearchController: indexPath doesn't change with original array

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.

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

Implementation of tableView cellForRowAt with a nested function

In on of the git repositories I found pretty weird implementation of tableView cellForRowAt method.
The method have inside implementation of configureCell method. Isn't that wrong ? Let say.. for 1000 cells in table view - it is going to create this method about 1000 thousand times ? Can any one tell me if it is good way in swift or not ? I suppose it is totally wrong.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func configureVehicleCell(_ object: CustomModel) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCellTableViewCell
if object.hasIssues {
cell.dvirImageView.isHidden = false
}
if object.cmv {
cell.cmvImageView.isHidden = false
}
cell.titleLabel.text = object.name
cell.subtitleLabel.text = object.someName
cell.tag = Int(object.id)
return cell
}
if lastUsedId == nil {
return configureCell(objectArray[(indexPath as NSIndexPath).row])
} else {
if (indexPath as NSIndexPath).section == 0 {
return configureCell(lastUsed!)
} else {
return configureCell(objectsArray[(indexPath as NSIndexPath).row])
}
}
}
In my opinion it should look like:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if lastUsedId == nil {
return configureCell(objectArray[(indexPath as NSIndexPath).row])
} else {
if (indexPath as NSIndexPath).section == 0 {
return configureCell(lastUsed!)
} else {
return configureCell(objectsArray[(indexPath as NSIndexPath).row])
}
}
}
func configureVehicleCell(_ object: CustomModel) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCellTableViewCell
if object.hasIssues {
cell.dvirImageView.isHidden = false
}
if object.cmv {
cell.cmvImageView.isHidden = false
}
cell.titleLabel.text = object.name
cell.subtitleLabel.text = object.someName
cell.tag = Int(object.id)
return cell
}

Userdefaults Tableview placemark Swift 3

I am using uitableview. I am trying to save the placemark when i have selected a cell. From the code below i have added a tableview and when i select a cell the placemark will be shown. But i'm having trouble saving it because when i go to another view controller and go back it doesn't show. I have researched and find that i need to use UserDefaults but i have no clue how to use it. can someone point me how can i achieve this. Thanks
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
let contactsCell = app.helper.contacts[indexPath.row]
cell!.textLabel?.text = contactsCell
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let contactToCall = app.helper.contacts[indexPath.row]
app.helper.contactSelected = contactToCall
if let cell = tableView.cellForRow(at: indexPath) {
if cell.isSelected {
cell.accessoryType = .checkmark
}
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .none
}
}
You don't need UserDefaults unless you want to maintain state after the app has been closed and restarted, and once you start storing user defaults, you're likely to want to store more - so it makes sense to create a defaults data class to store the selected index and any other 'stuff' you may need.
In this example, I'm going to load the defaults on viewDidLoad and store the defaults on each select / deselect. If you have other values stored, you should have additional storage calls.
Create a class for storing your data
class MyDefaultsData : NSObject, NSCoding
{
var selectedIndexPath : IndexPath?
var otherStuff : String?
override init()
{
// you can set up default values here if you need them
selectedIndexPath = nil
otherStuff = nil
}
// archiving code
func encode(with aCoder: NSCoder)
{
// you can't store IndexPath directly, so split it into row and section
if selectedIndexPath == nil
{
// set invalid values which we can identify later
aCoder.encode(-1, forKey: "indexSection")
aCoder.encode(-1, forKey: "indexRow")
}
else
{
aCoder.encode(selectedIndexPath!.section, forKey: "indexSection")
aCoder.encode(selectedIndexPath!.row, forKey: "indexRow")
}
aCoder.encode(otherStuff, forKey: "otherStuff")
}
required init(coder aDecoder: NSCoder)
{
let indexSection = aDecoder.decodeInteger(forKey: "indexSection")
let indexRow = aDecoder.decodeInteger(forKey: "indexRow")
if indexSection != -1 && indexRow != -1 // if no row is selected, these values will both be -1
{
selectedIndexPath = IndexPath(row: indexRow, section: indexSection)
}
else
{
selectedIndexPath = nil
}
self.otherStuff = aDecoder.decodeObject(forKey: "otherStuff") as? String
}
}
Define a variable of your default data within the ViewController
var defaultData = MyDefaultsData()
Load the data in your viewDidLoad
override func viewDidLoad()
{
loadDefaults()
}
func loadDefaults()
{
var dataDefaults : MyDefaultsData?
if let data = UserDefaults.standard.object(forKey: "NSDefaultsTest") as? Data
{
dataDefaults = NSKeyedUnarchiver.unarchiveObject(with: data) as? MyDefaultsData
self.defaultData.otherStuff = dataDefaults?.otherStuff ?? "No value found"
self.defaultData.selectedIndexPath = dataDefaults?.selectedIndexPath
}
}
then you use that default data within cellForRowAt
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
let contactsCell = app.helper.contacts[indexPath.row]
cell!.textLabel?.text = contactsCell
// set or clear the checkmark
if selectedIndexPath == indexPath
{
cell.accessoryType = .checkmark
}
else
{
cell.accessoryType = .none
}
return cell!
}
When you change the selected row, you need to reload both the previous selected row (if any) and the currently selected row, to get the checkmark drawn
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let previousIndexPath = defaultData.selectedIndexPath
if previousIndexPath != indexPath
{
defaultData.selectedIndexPath = indexPath
}
else
{
defaultData.selectedIndexPath = nil
}
storeDefaults()
if previousIndexPath != nil
{
tableView.reloadRows(at: [previousIndexPath!], with: .automatic)
}
if defaultData.selectedIndexPath != nil
{
tableView.reloadRows(at: [defaultData.selectedIndexPath!], with: .automatic)
}
}

How to Set data to second tableview in same Class in swift

How to Set data to second tableview in same Class in swift.I am using two tables in the the same controller one for dropdown and another for listing. I am unable to set data to second table(listing) in class
as else part is not called in cellForRowAtIndexPath. Thanks in Advance
import UIKit
class PunchClockVC: UIViewController , UITableViewDataSource, UITableViewDelegate{
var appdel = UIApplication.sharedApplication().delegate as! AppDelegate
#IBOutlet weak var dropdownTable: UITableView!
#IBOutlet weak var mainTable: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.mainTable.registerClass(PunchClockCustomCell.self, forCellReuseIdentifier: "PunchClockCustomCell")
self.dropdownTable.registerClass(UITableViewCell.self, forCellReuseIdentifier: "dropdowncell")
self.dropdownTable.hidden = true
}
#IBAction func textFieldTapped(sender: AnyObject) {
if self.dropdownTable.hidden == true {
self.dropdownTable.hidden = false
}
else{
self.dropdownTable.hidden = false
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == dropdownTable {
return jobArrayID.count
}
return 8
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if tableView == self.dropdownTable {
let cell = tableView.dequeueReusableCellWithIdentifier("dropdowncell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = (jobArrayID[indexPath.row] as! String) + "-" + (jobArrayName[indexPath.row] as! String)
return cell
}
else {
let cell1 = tableView.dequeueReusableCellWithIdentifier("PunchClockCustomCell", forIndexPath: indexPath) as! PunchClockCustomCell
if indexPath.row == 0
{
cell1.jobcell?.font = UIFont(name: "MuseoSlab-500", size: 25.0)
cell1.locationcell?.font = UIFont(name: "MuseoSlab-500", size: 25.0)
cell1.timecell?.font = UIFont(name: "MuseoSlab-500", size: 25.0)
cell1.typecell?.font = UIFont(name: "MuseoSlab-500", size: 25.0)
cell1.jobcell?.textColor = UIColor.blackColor()
cell1.locationcell?.textColor = UIColor.blackColor()
cell1.timecell?.textColor = UIColor.blackColor()
cell1.typecell?.textColor = UIColor.blackColor()
cell1.jobcell?.text = "Job"
cell1.locationcell?.text = "Location"
cell1.timecell?.text = "Time"
cell1.typecell?.text = "Type"
// return cell1
}
else {
cell1.jobcell?.text = "Jobdata"
cell1.locationcell?.text = "Locationdata"
cell1.timecell?.text = "Timedata"
cell1.typecell?.text = "OUT"
// return cell1
}
return cell1
}
}
It's quite simple You need to set it only with cellForRowAtIndexPath method but the main thing to do is you need to code like below
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
if tableView == firstTbaleView
//outlet given to first tableView
{
let cell = tableView.dequeueReusableCellWithReuseIdentifier("cell1", forIndexPath: indexPath) as! cust1TableViewCell
cell.imgView.image = images[indexPath.row]
cell.filtLabel.text = self.filtersCount[indexPath.row]
return cell
}}else {
let cell2 = tableView.dequeueReusableCellWithReuseIdentifier("cell2", forIndexPath: indexPath) as! cust2TableViewCell
cell2.imgview.image = UIImage(named: colPhotos[indexPath.row])
cell2.labl.text = colNames[indexPath.row]
// cell2.layer.borderColor = UIColor.blueColor().CGColor
// cell2.layer.borderWidth = 2.0
return cell2
}
and that's it you can ask me for any help..
Lets make this a little safer:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == self.dropdownTable {
return jobArray.count
}
else if tableView == self.mainTable {
return = 5
}
//Default return 0. This way if references are broken or change, you won't crash
return 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if tableView == self.dropdownTable {
let cell = tableView.dequeueReusableCellWithIdentifier("dropdowncell", forIndexPath: indexPath) as! UITableViewCell
//configure your cell
return cell
}
else if tableView == self.mainTable {
let cell = tableView.dequeueReusableCellWithIdentifier("PunchClockCustomCell", forIndexPath: indexPath) as! PunchClockCustomCell
//configure your cell
return cell
}
//Shouln't ever reach here, but again, if we refactor somewhere then we'll see an error show up before here.
return UITableViewCell()
}