Swift - Handling custom checkmark button state on multiple sections of tableview - ios

I have a tableview with multiple sections each section is having 3 cells , each cell contains the custom check mark button. Where user can change check and uncheck images of check button on click.
The problem is, i am changing the cell button image from uncheck to check when user click on button which is working fine. If i scroll the tableview that check mark image is adding to wrong cell.
I googled it and found the solution like saving clicked cell indexPath.row in array and removing from array if user click again on same cell.
It is not working as i have multiple sections.
Please provide me any suggestion to find out the solution for my problem.
// Button action.
func buttonTappedOnCell(cell: customeCell) {
let indexPath : IndexPath = self.tableView.indexPath(for: cell)!
if self.selectedIndexPath.count > 0 {
for selectedIndex in self.selectedIndexPath {
if selectedIndex as! IndexPath == indexPath {
self.selectedIndexPath.remove(indexPath)
} else {
self.selectedIndexPath.add(indexPath)
}
}
} else {
self.selectedIndexPath.add(indexPath)
}
Code in cellForRowAtIndexPath
for anIndex in self.selectedIndexPath {
if anIndex as! IndexPath == indexPath {
cell.checkMarkButton.setBackgroundImage(UIImage(named: "checkMark"), for: .normal)
} else {
cell.checkMarkButton.setBackgroundImage(UIImage(named: "UnCheckMark"), for: .normal)
}
}

You can simplify your code greatly by using a Set<IndexPath>. There is no need to loop through an array.
var selectedPaths=Set<IndexPath>()
func buttonTappedOnCell(cell: customeCell) {
if let indexPath = self.tableView.indexPath(for: cell) {
var image = UIImage(named: "CheckMark")
if self.selectedPaths.contains(indexPath) {
image = UIImage(named: "UnCheckMark")
self.selectedPaths.remove(indexPath)
} else {
self.selectedPaths.insert(indexPath)
}
cell.checkMarkButton.setBackgroundImage(image, for: .normal)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! YourCellType
...
var image = UIImage(named: "UnCheckMark")
if self.selectedPaths.contains(indexPath) {
image = UIImage(named: "checkMark")
}
cell.checkMarkButton.setBackgroundImage(image, for: .normal)
return cell
}

This happens because the cells of a tableView are reused to minimise memory usage.
If you are using a custom implementation of check button then you have to store the values for the cell which is selected in a data Structure like an array for example if you have 2 sections with 3 elements in each section with 2nd item in 1st section selected, then the data structure like an array can be like [[false, true, false], [false, false. false]].
Here I am setting true in case the cell is selected. Update the values of this data structure and check from it when applying the image in the cellForRowAt and apply the checkedImage only when the indexPath.section and indexPath.row for Array return a true Bool.
Hope this helps. Feel free to reach out in case of any doubts. Happy coding.

inside of your TableView DidSelect method just put
let cell = tableView.cellForRowAtIndex(indexPath).accessoryType = UITableViewCell.accessoryType = Checkmark
vice versa for DidDeselect method but change the Checkmark into None.
let cell = tableView.cellForRowAtIndex(indexPath).accessoryType = UITableViewCell.accessoryType = None
If you create more complex code for a simple method, It'll give you disaster.

Related

Multiple sections in UITableView and UIButtons swift

I have a UITableView that has sections (Category0, Category1,..), and every row of a specific section is a UITableView that has one section which is the question (Question1,..) and rows which are the options to be answered (option1, option2,..).
The problem is when I click on a button in a specific category and a specific question (Category0, question1, option0) see screenshot1,
immediately another buttons in another categories are clicked (Category1, question2, option0) see screenshot2,
and (Category4, question1, option0) see screenshot3.
the code below:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as? insideTableViewCell
cell?.answerlabel.text = "option \(indexPath.row)"
cell?.initCellItem(id: (myObject?.id)! , answer: (myObject?.answerArray![indexPath.row] as? String)!)
return cell!
}
In a custom UITableViewCell which is insideTableViewCell:
func initCellItem(id: Int , answer: String) {
radioButton.setImage( imageLiteral(resourceName: "unchecked"), for: .normal)
radioButton.setImage( imageLiteral(resourceName: "checked"), for: .selected)
radioButton.tag = id
radioButton.setTitle(answer, for: UIControlState.disabled)
radioButton.addTarget(self, action: #selector(self.radioButtonTapped), for: .touchUpInside)
}
#objc func radioButtonTapped(_ radioButton: UIButton) {
print(radioButton.tag)
print(radioButton.title(for: UIControlState.disabled) as Any)
let answer = radioButton.title(for: UIControlState.disabled) as Any
let StrId = String(radioButton.tag)
defaults.set(answer, forKey: StrId)
let isSelected = !self.radioButton.isSelected
self.radioButton.isSelected = isSelected
if isSelected {
deselectOtherButton()
}
}
func deselectOtherButton() {
let tableView = self.superview as! UITableView
let tappedCellIndexPath = tableView.indexPath(for: self)!
let section = tappedCellIndexPath.section
let rowCounts = tableView.numberOfRows(inSection: section)
for row in 0..<rowCounts {
if row != tappedCellIndexPath.row {
let cell = tableView.cellForRow(at: IndexPath(row: row, section: section)) as! insideTableViewCell
cell.radioButton.isSelected = false
}
}
}
You haven't posted code still guessing.
You can create model object like
class QuestionData {
var strQuestion:String? // This may contains Question
var strOptions:[String]? // It may contains options titles of your buttons
var selectedAnswerIndex:Int? // When any button tapped
}
And you should create category models like
class Categories {
var categoryTitle:String?
var questions:[QuestionData] = []
}
you can use this Categories class as main source of your dataSource array
var arrayDataSource = [Categories]()
And fill this with your original data.
now whenever any button tapped you can use selectedAnswerIndex:Int to store current selected option for question. and if it is null then user has not selected any option yet.
I have created class so it is reference type you can directly set the value without worry
Hope it is helpful to you
There has some and simple code I think it will help you :- if it is not sutable for you pls don't mind :-
if (!btnGreen3.isSelected)
{
btnGreen3.isSelected = !btnGreen3.isSelected
}
btnBlue3.isSelected = false
btnBlack3.isSelected = false
You need to save the states of every cell.
The reason is you are using dequereuseable cell with identifier when you scroll it switch to another cell.
So make Array or Dictionary where save the state of every selected and unselected Rows.

how to store tableview cell checkmark state in popover?

I am having few buttons in initial view controller. A popover with an expandable tableview will appear on click of any button. I am using single popover for all the buttons with different data to be shown on tableview. The selected items in the popover when I am clicking on the first button is being removed when I am clicking on the other buttons so as to all. The selected items is gonna saved into an array based on indexpath.row. An exception error is coming while I am trying to remove the selected items from the same array based on same indexpath.row.
How can i store the checkmark on each cell selection for all popover ?
Here is a piece of my code
func expandableTableView(_ expandableTableView: LUExpandableTableView, didSelectRowAt indexPath: IndexPath)
{
let selectedService = arrData[indexPath.section].Children?[indexPath.row].EnglishName ?? ""
let inPrice = arrData[indexPath.section].Children?[indexPath.row].InPrice ?? 0
print("service and price : \(selectedService) \(inPrice)")
let selectedItem = (" \(selectedService) \(inPrice)")
let cell: UITableViewCell? = expandableTableView.cellForRow(at: indexPath)
if cell?.accessoryType == UITableViewCellAccessoryType.none
{
cell?.accessoryType = UITableViewCellAccessoryType.checkmark
selectionArr.append(selectedItem)
inPriceCount = inPrice
}
else
{
cell?.accessoryType = UITableViewCellAccessoryType.none
selectionArr.remove(at: indexPath.row)
inPriceCount = -inPrice
}
self.delegate?.messageData(data: selectionArr as AnyObject)
self.delegate?.inPrice(data: inPriceCount as AnyObject)
let rowToSelect = [expandableTableView .indexPathForSelectedRow]
print(rowToSelect)
}
You should not remove any thing from array. Just add a key on every index
dict["ischeckMarked"]=false
When you change tap on checkmark change it true
dict["ischeckMarked"]=true
If already checked and now you want to unmark change it to false
dict["ischeckMarked"]=false
Hope this will help you out.

UITableViewCell images disappear when scrolling

I have a UIButton inside a UITableViewCell where the image changes once the button is tapped. Though the selected buttons get selected as intended, once the UITableView scrolls, the selected images disappear since the cells are reused.
I'm having trouble writing the logic. Please help.
My code is below, in Swift 3.
CellForRow:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
//Button_Action
addSongButtonIdentifier(cell: cell, indexPath.row)
}
This is where the cell is created:
func addSongButtonIdentifier(cell: UITableViewCell, _ index: Int) {
let addButton = cell.viewWithTag(TABLE_CELL_TAGS.addButton) as! UIButton
//accessibilityIdentifier is used to identify a particular element which takes an input parameter of a string
//assigning the indexpath button
addButton.accessibilityIdentifier = String (index)
// print("visible Index:",index)
print("Index when scrolling :",addButton.accessibilityIdentifier!)
addButton.setImage(UIImage(named: "correct"), for: UIControlState.selected)
addButton.setImage(UIImage(named: "add_btn"), for: UIControlState.normal)
addButton.isSelected = false
addButton.addTarget(self, action: #selector(AddToPlaylistViewController.tapFunction), for:.touchUpInside)
}
The tap function:
func tapFunction(sender: UIButton) {
print("IndexOfRow :",sender.accessibilityIdentifier!)
// if let seporated by a comma defines, if let inside a if let. So if the first fails it wont come to second if let
if let rowIndexString = sender.accessibilityIdentifier, let rowIndex = Int(rowIndexString) {
self.sateOfNewSongArray[rowIndex] = !self.sateOfNewSongArray[rowIndex]//toggle the state when tapped multiple times
}
sender.isSelected = !sender.isSelected //image toggle
print(" Array Data: ", self.sateOfNewSongArray)
selectedSongList.removeAll()
for (index, element) in self.sateOfNewSongArray.enumerated() {
if element{
print("true:", index)
selectedSongList.append(songDetailsArray[index])
print("selectedSongList :",selectedSongList)
}
}
}
There is logical error in func addSongButtonIdentifier(cell: UITableViewCell, _ index: Int) function at line addButton.isSelected = false
it should be addButton.isSelected = self.sateOfNewSongArray[index]
Since, cellForRowAtIndexpath method is called every time table is scrolled, it's resetting selected state of 'addButton'
You need to have array where you store which indexes are selected like selectedSongList array that you have. Then in your cellForRow method you need to use bool proparty from this array to give selected or deselected state to your button or in your addSongButtonIdentifier method selected state need to be
addButton.isSelected = selectedSongList.contains(indexPath.row)
Create a Model class for filling UITableView and take UIImage varaivals in that model, which will hold the current image for cell. On click on button action just change the UIImage variable with current image.
Best approach will be using a model class and keeping the track of each indiviual element in cell. But let me give you a quick fix.
Create a custom class of Button any where like this.
class classname: UIButton {
var imageName: String?
}
Go in your storyboard change the class from UIButton to classname
In your tableViewCellForIndexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let addButton = cell.viewWithTag(TABLE_CELL_TAGS.addButton) as! classname
if let imgName = addButton.imageName {
addButton.setImage(UIImage(named: imgName), for: UIControlState.normal)
} else {
addButton.setImage(UIImage(named: "add_btn"), for:UIControlState.normal)
}
addButton.addTarget(self, action: #selector(AddToPlaylistViewController.tapFunction), for:.touchUpInside)
return cell
}
Now let's move to your tapbutton implementation
func tapFunction(sender: classname) {
print("IndexOfRow :",sender.accessibilityIdentifier!)
// if let seporated by a comma defines, if let inside a if let. So if the first fails it wont come to second if let
if let rowIndexString = sender.accessibilityIdentifier, let rowIndex = Int(rowIndexString) {
self.sateOfNewSongArray[rowIndex] = !self.sateOfNewSongArray[rowIndex]//toggle the state when tapped multiple times
}
sender.imageName = sender.imageName == "correct" ? "add_btn" : "correct" //image toggle
sender.setImage(UIImage(named: sender.imageName), for:UIControlState.normal)
print(" Array Data: ", self.sateOfNewSongArray)
selectedSongList.removeAll()
for (index, element) in self.sateOfNewSongArray.enumerated() {
if element{
print("true:", index)
selectedSongList.append(songDetailsArray[index])
print("selectedSongList :",selectedSongList)
}
}
}

Multiple buttons in UITableViewCell in a 'top-down' UITableView

I am trying to work out how to action buttons within a UITableViewCell when the table view is 'top down' - as in, each new row is added to the top of the table view. I achieve this with the following code:
I use the following code to insert new items into my model array and then into the tableview:
let newItem = Item(text: inputTextView.text, time: Date())
items.insert(newItem, at: 0) //inserting into the front of model array
tableView.beginUpdates()
let indexPath:IndexPath = IndexPath(row:0, section:0)
tableView.insertRows(at: [indexPath], with: .fade)
tableView.endUpdates()
Within the cellForRowAt function I run the following code:
let cell = tableView.dequeueReusableCell(withIdentifier: postCellID, for: indexPath) as! NewPostCell
let item = items[indexPath.row]
cell.postTextLabel.text = text
cell.timeLabel.text = dateFormatter.string(from: time)
cell.selectionStyle = .none
return cell
Each of my tableview cells have three buttons in it.
How do I connect up these buttons so I know which button is pressed from which indexPath?
The problem is that if I use indexPath.row to tag the buttons, then the buttons in all cells gets tagged with 0, as each insert is happening at the top of the table at indexPath.row 0th position.
I thought of tagging the buttons with the current size of my model array, but that doesn't work either as when cells are re-used they could then be tagged with the length of the array at that point, which would be wrong.
There are a lot of apps that have 'last entry at the top' of the tableview sort of setup, with buttons in cells. So there must be a way to do this.
You can add UIButton to your UITableViewCell and access these UIButton via tag and add target method to these buttons as:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// create a new cell if needed or reuse an old one
let cell:UITableViewCell = self.tableVw.dequeueReusableCell(withIdentifier: "Cell") as UITableViewCell!
//Access UIButton
let button1:UIButton = cell.viewWithTag(10) as! UIButton
let button2:UIButton = cell.viewWithTag(11) as! UIButton
let button3:UIButton = cell.viewWithTag(12) as! UIButton
//Add Action Methods to UIButtons
button1.addTarget(self, action: #selector(FisrtButtonClick), for: .touchUpInside)
button2.addTarget(self, action: #selector(SecondButtonClick), for: .touchUpInside)
button3.addTarget(self, action: #selector(ThirdButtonClick), for: .touchUpInside)
return cell
}
Button Actions
// MARK: - UIButton Methods.
func FisrtButtonClick(_ sender: Any) {
//Get Button cell position.
let ButtonPosition = (sender as AnyObject).convert(CGPoint.zero, to: tableVw)
let indexPath = tableVw.indexPathForRow(at: ButtonPosition)
if indexPath != nil {
print("Cell indexPath: \(indexPath?.row)")
}
}
func SecondButtonClick(_ sender: Any) {
//Get Button cell position.
let ButtonPosition = (sender as AnyObject).convert(CGPoint.zero, to: tableVw)
let indexPath = tableVw.indexPathForRow(at: ButtonPosition)
if indexPath != nil {
print("Cell indexPath: \(indexPath?.row)")
}
}
func ThirdButtonClick(_ sender: Any) {
//Get Button cell position.
let ButtonPosition = (sender as AnyObject).convert(CGPoint.zero, to: tableVw)
let indexPath = tableVw.indexPathForRow(at: ButtonPosition)
if indexPath != nil {
print("Cell indexPath: \(indexPath?.row)")
}
}
Main idea
These are the basic steps you have to do:
Set unique tag values for each of the 3 button types: this will allow to identify which of the 3 buttons of a cell is pressed. (for instance tag value 1 for the first of your buttons, value 2 for the second kind, value 3 for the third button).
Link the 3 buttons to a #IBAction method.
You can also do this programmatically with target-actions: call aButton.addTarget(target:, action:, for:) for each button.
Then when a button will be pressed, you will use an helper function to determine the cell index of the button that was pressed.
Code
The #IBAction method should look like #IBAction func buttonTrigerred(_ sender: UIButton){...} and the code would be:
#IBAction func buttonTrigerred(_ sender: UIButton){
// -- retrieving the index path of the cell, as #NikhleshBagdiya posted
// Determine the indexPath of the cell
let buttonPosition = (sender as AnyObject).convert(CGPoint.zero, to: tableView)
let cellIndexPath = tableView.indexPathForRow(at: buttonPosition)
// --
// Determine which button is called
if sender.tag == 1 { ... } // the first kind of button has been pressed
else if sender.tag == 2 { ... } // second button kind
else if sender.tag == 3 { ... } // third button kind
}
Setting the tag value
Each of my tableview cells have three buttons in it.
I suppose you have designed your UITableViewCell in a Storyboard or Xib file. So for each of the 3 buttons of your cell: select the button, go to the attributes inspector, set the tag to the custom values indicated above.

filterTableViewController.reloadRows reloading rows only on first call

I have a table with 3 rows each with check button.What I am doing is when I select all the three buttons I want to click my cancel button which is on view not table on same controller to reload all 3 rows the call goes to custom cell class where uncheck is set to true and rows are reloaded.For the first attempt it works fine I can see correct index to be reloaded.On the second time again when I select all 3 check buttons and click cancel again I can see correct index to be reloaded but the call is not going to custom cell class again the check box still remains checked.Any idea why?
I am always getting correct index in my array.
Cancel button code-:
#IBAction func cancelDataItemSelected(_ sender: UIButton) {
for index in selectedButtonIndex{
let indexPath = IndexPath(item: index, section: 0)
print(selectedButtonIndex)
filterTableViewController.reloadRows(at: [indexPath], with: UITableViewRowAnimation.none)
}
selectedButtonIndex .removeAll()
print(selectedButtonIndex)
}
Table code-:
extension filterControllerViewController:UITableViewDataSource,UITableViewDelegate
{
// NUMBER OF ROWS IN SECTION
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return ControllerData.count
}
// CELL FOR ROW IN INDEX PATH
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let Cell = tableView.dequeueReusableCell(withIdentifier: "filterCell", for: indexPath) as! ControllerCellTableViewCell
Cell.filterTableMenu.text = ControllerData[indexPath.item]
Cell.radioButtonTapAction = {
(cell,checked) in
if let radioButtonTappedIndex = tableView.indexPath(for: cell)?.row{
if checked == true {
self.selectedButtonIndex.append(radioButtonTappedIndex)
}
else{
while self.selectedButtonIndex.contains(radioButtonTappedIndex) {
if let itemToRemoveIndex = self.selectedButtonIndex.index(of: radioButtonTappedIndex) {
self.selectedButtonIndex.remove(at: itemToRemoveIndex)
}
}
}
}
}
return filterCell
}
Custom Class-:
var radioButtonTapAction : ((UITableViewCell,Bool)->Void)?
//MARK-:awakeFromNib()
override func awakeFromNib() {
super.awakeFromNib()
filterTableSelectionStyle()
self.isChecked = false
}
// CHECKED RADIO BUTTON IMAGE
let checkedImage = (UIImage(named: "CheckButton")?.withRenderingMode(UIImageRenderingMode.alwaysOriginal))! as UIImage
// UNCHECKED RADIO BUTTON IMAGE
let uncheckedImage = (UIImage(named: "CheckButton__Deselect")?.withRenderingMode(UIImageRenderingMode.alwaysOriginal))! as UIImage
// Bool STORED property
var isChecked: Bool = false {
didSet{
// IF TRUE SET TO CHECKED IMAGE ELSE UNCHECKED IMAGE
if isChecked == true {
TableRadioButton.setImage(checkedImage, for: UIControlState.normal)
} else {
TableRadioButton.setImage(uncheckedImage, for: UIControlState.normal)
}
}
}
// FILTER CONTROLLER RADIO BUTTON ACTION
#IBAction func RadioButtonTapped(_ sender: Any) {
isChecked = !isChecked
radioButtonTapAction?(self,isChecked)
}
Fundamental misunderstanding of how "reusable" table cells work.
Let's say your table view is tall enough that only 8 cells are ever visible. It seems obvious that 8 cells will need to be created, and they will be reused when you scroll.
What may not be obvious is that the cells also are reused when they are reloaded. In other words, every time .reloadData is called - even if you are only reloading one cell that is currently visible - that cell is reused. It is not re-created.
So, the key takeaway point is: Any initialization tasks happen only when the cell is first created. After that, the cells are reused, and if you want "state" conditions - such as a checked or unchecked button - it is up to you to "reset" the cell to its original state.
As written, your cellForRowAt function only sets the .filterTableMenu.text ... it ignores the .isChecked state.
You can mostly fix things just by setting the cell's .isChecked value, but you're also tracking the on/off states in a much more complicated manner than need be. Instead of using an Array to append / remove row indexes, use an Array of Booleans, and just use array[row] to get / set the values.
Then your cellForRowAt function will look about like this:
// CELL FOR ROW IN INDEX PATH
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let filterCell = tableView.dequeueReusableCell(withIdentifier: "filterCell", for: indexPath) as! ControllerCellTableViewCell
// set the label in filterCell
filterCell.filterTableMenu.text = ControllerData[indexPath.item]
// set current state of checkbox, using Bool value from out "Tracking Array"
filterCell.isChecked = self.selectedButtonIndex[indexPath.row]
// set a "Callback Closure" in filterCell
filterCell.radioButtonTapAction = {
(checked) in
// set the slot in our "Tracking Array" to the new state of the checkbox button in filterCell
self.selectedButtonIndex[indexPath.row] = checked
}
return filterCell
}
You can see a working example here: https://github.com/DonMag/CheckBoxCells
Remember that the cells are reused and that reloadRows just tells the rows to redraw. When a checkbox in a cell is checked by the user, the new checked state should be saved in the underlying data source, and the state marked in the cell in cellForRowAtIndexPath. Otherwise the cell checkbox shows the state for the last time it was set by the user for all indices and not the state for the underlying data source.

Resources