I created a simple demo app for this problem, which is not vital but I am looking for an elegant solution. I have a grouped tableview with multiple sections, each allowing a single item selection. Here is the code:
class TableViewController: UITableViewController {
let sections = ["A", "B", "C", "D", "E", "F"]
let items = [
["one", "two", "three", "four"],
["one", "two", "three", "four"],
["one", "two", "three", "four"],
["one", "two", "three", "four"],
["one", "two", "three", "four"],
["one", "two", "three", "four"]
]
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return sections.count
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section]
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items[section].count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) // option 1
// let cell = UITableViewCell(style: .Default, reuseIdentifier: "Cell") // option 2
cell.textLabel?.text = items[indexPath.section][indexPath.row]
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
let cell = tableView.cellForRowAtIndexPath(indexPath)
let section = indexPath.section
for rowNumber in 0..<tableView.numberOfRowsInSection(section) {
let tempPath = NSIndexPath(forRow: rowNumber, inSection: section)
let tempCell = tableView.cellForRowAtIndexPath(tempPath)
tempCell?.accessoryType = .None
}
cell?.accessoryType = .Checkmark
}
}
The table is long which implies scrolling to see all items. Now, if I take option 1 (dequeueReusableCellWithIdentifier) and select a row in section 1, automatically a row in e.g. section 5 gets selected (which can be seen by scrolling). Obviously this is not intended. If I take option 2 then this does not happen, but the row gets deselected as soon as it disappears from the view after scrolling. Also not intended.
My solution so far has been to add a variable selectedCellIndices which is an array holding the selected indexPath.row for each section. That works, but is rather ugly.
Any suggestions for a neat solution?
The Issue:
The moment a row scrolls of the screen and a new row becomes visible swift, the cell of the row that scrolled off the screen is reused for the row that just became visible. Currently you are only setting checkmark on the cell. One thing to remember is that ROWs are the data and CELLs are the views for that data.
If you update the checkMark on the cell then that is only updating the view. And when the row scrolls off the screen that cell is recycled or reused.
The Solution:
Apply a MVC technique. When you update your cell by placing or removing the checkmark you are only updating the view. Next you also need to update the Model. By Model I mean the Data Model.
Try creating a Data class or a data model:
import Foundation
class ChecklistItem {
var text = ""
var checked = false
func toggleChecked() {
checked = !checked
}
}
Within your controller create an array that will hold data of the above declared class models data type like so:
var items: [ChecklistItem]
And within the didSelectRowAtIndex function you do the following:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if let cell = tableView.cellForRowAtIndexPath(indexPath) {
// get the object from the array that was tapped by using the index path of the row
let item = items[indexPath.row]
// Toggle the checkMark, this method was declared within the data model as it was responsible for manipulating or updating the data.
item.toggleChecked()
if item.checked {
cell.accessoryType = .Checkmark
} else {
cell.accessoryType = .None
}
}
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
The above is your data model. Now when you updated your cell, also update your data model by stating the checked variable for that object true or false. True means theres a check mark for that row/cell and false means no check mark. This way you can persist your check marks. Also you need to save these objects into the above declared items array.
Let me know if you need more info. Or if somethings confusing. I hope this helps.
Option 1:
Enable allowsMultipleSelection on your tableView.
in tableView:didSelectRowAtIndexPath:, check if you already have a selected cell in that section (a bit of filtering on tableView.indexPathsForSelectedRows) . If that's the case, deselect it.
This will use the standard iOS selection style (the coloured background), but you can change that in your custom table cell's setSelected: (to add/remove the accessory), and configuring the table cell's selectionStyle to None. Remember to call super in your setSelected, you still need to set the internal selected flag.
Option 2: do what you were suggesting with keeping the selection in a separate array, using it to configure the accessory when the a table view cell is dequeued.
Obviously, you should add cell.accessoryType = .None to cellForRowAtIndexPath, because cells are reused without changing their last appearance.
Related
I have a tableview, where each cell is a custom tableview cell. That custom tableview cell contains a tableview and a label.
Lets say, the outer tableview is called MainTableView. Each cell of MainTableView consists another tableview. The problem is , when I select
inner tableview cell one by one the previously selected cell is not get deselected.In first image I have selected cell contains text “Two”. Then In the second image I have selected cell contains text “Five” but perviously selected cell “Two” still in selection mode. I want to deselect the previous cell when select a new one.
I have tried
tableView.deselectRow(at: IndexPath, animated: Bool)
this method inside didSelectRowAt in custom tableviewcell class but it didn’t serve the purpose because the previous indexPath is from seperate tableview. So, how can I deselect the previous one?
to get the correct inner tableView,
Firstly , you should record the outside tableView's cell indexPath, which cell the inner tableView is in.
So your should record two indexPathes
var selectionRowInfo: (lastOutsideIP: IndexPath?, lastInnerIP: IndexPath?)
Secondly, you get the correct tableView, via the outsideTableView.
if the inner table is visible, you shall handle it immediately. through outTableView.indexPathsForVisibleRows
the else condition, you need not handle it. The tableView reuse mechanism will refresh its state.
// pseudo code.
if let lastOut = lastOutsideIP, let visibleRows = outTableView.indexPathsForVisibleRows, visibleRows.contains(lastOut){
let cell = tableView.cellForRow(at: lastOut) as! YourCell
// get the correct inner tableView via the cell
}
Because the inner tableViews are not related to each other. Select the cell of table one, will not affect the selection of cell of table two.
So your should build the connection manually.
Use a property to store the state var lastIndexPath: IndexPath?,
then every time select a indexPath,
// pseudo code.
if let last = lastIndexPath{
tableView.deselectRow(at: last, animated: true)
}
Please notice that, you should find the correct inner tableView, which has the lastIndexPath
The previous answer is along the right lines but has a flaw - it cannot distinguish between tableViews, which is the most important part. Also if the tableViews have different numbers of rows, it risks trying to access a row that doesn't exist and causing a crash.
To track the selected row in two tableViews (tv1 & tv2) you'll need to hold the selected row in each:
var tv1, tv2: UITableView!
var lastRowForTV1, lastRowForTV2: IndexPath?
and then respond to selections by identifying the tableView being used and adjusting the other (this assumes the two tableViews use the same datasource/delegate)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView === tv1 {
lastRowForTV1 = indexPath
if let last = lastRowForTV2 {
tv2.deselectRow(at: last, animated: true)
lastRowForTV2 = nil
}
} else if tableView === tv2 {
lastRowForTV2 = indexPath
if let last = lastRowForTV1 {
tv1.deselectRow(at: last, animated: true)
lastRowForTV1 = nil
}
}
}
I have solved the problem by using the idea of first answer given by dengApro . The idea was to find the correct inner table which contains the previously selected cell.
I have two files one is ViewController.swift that contains the outer tableview MainTableView. Another one is CustomTableViewCell.swift with CustomTableViewCell.xib that contains the custom cell with tableview.
fileprivate var lastSelectedInnerTableView : Int = -1
fileprivate var lastSelectedRow: Int = -1
fileprivate var tableViewList :[UITableView] = []
I have added these 3 variables in CustomTableViewCell.swift file outside the class CustomTableViewCell. lastSelectedSection , lastSelectedRow these 2 variable are used to keep
track of the last selected inner tableview (lastSelectedInnerTableView) and the last selected cell of that inner tableView (lastSelectedRow). tableViewList variable is used to keep the
Inner tableView. Inside awakeFromNib() I have append the created inner tableviews.
override func awakeFromNib() {
super.awakeFromNib()
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
tableViewList.append(self.tableView) // append the created inner tableviews
}
Then inside didSelectRowAt I have deselect the previous one:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if lastSelectedInnerTableView != -1 && lastSelectedRow != -1{
self.oldIndexPath.section = 0
self.oldIndexPath.row = lastSelectedRow
tableViewList[lastSelectedInnerTableView].deselectRow(at: self.oldIndexPath, animated: true)
}
lastSelectedInnerTableView = self.innerTableId
lastSelectedRow = indexPath.row
}
In ViewController.swift I have set innerTableId inside cellForRowAt
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let customCell: CustomTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "customCell") as! CustomTableViewCell
customCell.innerTableId = indexPath.row
customCell.customCellActionDelegate = self
return customCell
}
Explanation:
I have a UITableView that is being populated from JSON. The purpose of the tableview is for the user to select individual row records and have the checkmark accessory appear as a result.
The issue is that while I can get the checkmark to appear for whichever row is selected the checkmark is applied to the row, not the record itself.
For example, if I have two rows in the tableview and I select the first row, a checkmark is applied to it, but after updating the API to remove the row and reloading the tableView the first-row disappears but the checkmark is applied to what was the second record.
This is what my didSelect method looks like:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let section = sections[indexPath.section]
structure = sections[indexPath.section].items
let theStructure = structure[indexPath.row]
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
This is how the structure is defined for the JSON:
struct Section {
let name : String
let items : [Portfolios]
}
struct Portfolios: Decodable {
let code: String
let maker: String
}
Essentially I need help applying the checkmark to the actual record itself not just the static row.
The most efficient way is to add the isSelected information to the data model (Portfolios)
struct Portfolios : Decodable {
var isSelected = false
// other members
}
You might add also CodingKeys to exclude isSelected from being decoded.
In cellForRowAt set the checkmark according to isSelected
let item = sections[indexPath.section].items[indexPath.row]
cell.accessoryType = item.isSelected ? .checkmark : .none
In didSelectRowAt toggle isSelected and reload the row
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
sections[indexPath.section].items[indexPath.row].isSelected.toggle()
tableView.reloadRows(at: [indexPath], with: .none)
}
First Take an empty array of record and then add checked record to this empty array. In Tableview cellforRow check if a record is in this empty array or not. If it is present in this empty array then display checkmark else remove the checkmark.
Tabelview cells get recycel. You need to overwrite prepareForReuse and clear the selected state there.
override func prepareForReuse() {
super.prepareForReuse()
isSelected = false
}
As for the top answer:
Following the state in your data model is fine too as you set the state for each cel (which solves your problem). BUT if you don't need the state other then for deleting entries, then storing the state is pointless. Thing is, all elements in your data model have the state of not selected. So why store an identical information? It is a redundant information that cost you memory for each element. In this case is overriding the prepareForReuse function the best approach.
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.
I am new to swift, and I am trying to take text from one textfield and values from two other text fields (one value from each text field) on a ViewController, and then when a button is pressed, have all three pieces of information displayed horizontally in a cell in a tableview.
This is an example of what I am trying am trying to do:
I would like the data from text fields one, two and three to be displayed in labels one, two and three, once the button is pressed.
#IBAction func btn_reload(sender: AnyObject)
{
/*lbl1.text = txtfield1.text!
lbl2.text = txtfield2.text!
lbl3.text = txtfield3.text!*/
var arrmute: [AnyObject] = [AnyObject]()
var arrmute2: [AnyObject] = [AnyObject]()
var arrmute3: [AnyObject] = [AnyObject]()
arrmute.append(txtfield1.text!)
arrmute2.append(txtfield2.text!)
arrmute3.append(txtfield3.text!)
self.tbl_out.reloadData()
}
First setup set delegate and datasource of UITabelView in your storyboard.
Give reuseIdentifier of row as given in code: cell
Make outlet of your UITableView.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return arrmute.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var cell: UITableViewCell
if cell == nil
{
cell = UITableViewCell(style: .Default, reuseIdentifier: "cell")
}
cell.lbl1.text = arrmute[indexPath.row]
cell.lbl2.text = arrmute2[indexPath.row]
cell.lbl3.text = arrmute3[indexPath.row]
return cell
}
Issue 1: Check Marks Keep Disappearing when scrolling.
Issue 2: Need help adding/removing from array with unique ID to prevent duplicates.
I am trying to insert/remove a cellTextLabel from an empty array. I can't seem to find a good solution. Here's what I've tried and why it failed.
Bad Option 1
// Error = Out of range (I understand why)
myArray.insert(cell.textLabel.text, atIndex: indexPath)
Bad Option 2
// I won't have a way to reference the array item afterwards when I need to remove it. Also, this option allows for the same string to be entered into the array multiple times, which is not good for me.
myArray.insert(cell.textLabel.text, atIndex: 0)
Below is the code so far, any help is greatly appreciated. Thank you.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let row = indexPath.row
let cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier("items", forIndexPath: indexPath) as UITableViewCell
var myRowKey = myArray[row]
cell.textLabel.text = myRowKey
cell.accessoryType = UITableViewCellAccessoryType.None
cell.selectionStyle = UITableViewCellSelectionStyle.None
return cell
}
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
let selectedCell = tableView.cellForRowAtIndexPath(indexPath) as UITableViewCell!
if selectedCell.accessoryType == UITableViewCellAccessoryType.None {
selectedCell.accessoryType = UITableViewCellAccessoryType.Checkmark
}
var selectedItem = selectedCell.textLabel.text!
println(selectedItem)
}
func tableView(tableView: UITableView!, didDeselectRowAtIndexPath indexPath: NSIndexPath!) {
let deSelectedCell = tableView.cellForRowAtIndexPath(indexPath) as UITableViewCell!
if deSelectedCell.accessoryType == UITableViewCellAccessoryType.Checkmark {
deSelectedCell.accessoryType = UITableViewCellAccessoryType.None
}
var deSelectedItem = deSelectedCell.textLabel.text!
println(deSelectedItem)
}
Issue 1: Your checkmarks keep disappearing when you're scrolling because of the following line in the didDeselectRowAtIndexPath method:
let deSelectedCell = tableView.cellForRowAtIndexPath(indexPath) as UITableViewCell!
The call to cellForRowAtIndexPath will create a NEW cell. It will NOT modify the currently visible cell on the screen in your UITableView. This is because cells are reused as items scroll on and off the screen, with new data loaded into them.
To retain the selection status of your cells, you will need to upgrade your data model a bit. Right now your data comes from the myArray which is a String array. You could try something as follows instead:
struct Item {
var name: String // The string value you are putting in your cell
var isSelected: Bool // The selected status of the cell
}
Then you would define your data something like this:
var myArray = [
Item(name: "Cell 1 value", isSelected: false),
Item(name: "Cell 2 value", isSelected: false),
...
]
And your tableView:didSelectRowAtIndexPath method would look more like this:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// Toggle the selected state of this cell (e.g. if it was selected, it will be deselected)
items[indexPath.row].isSelected = !items[indexPath.row].isSelected
// Tell the table to reload the cells that have changed
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
tableView.endUpdates()
// Reloading that cell calls tableView:numberOfRowsInSection and refreshes that row within the tableView using the altered data model (myArray array)
}
Next time you tap that row, the tableView:didSelectRowAtIndexPath method will fire again and toggle the selected state of that cell. Tell that cell to reload will refresh the cell that is actually visible on the screen.
Issue 2: Without knowing too much about the type of data you want to keep unique and how you are adding/removing in ways that could add duplicates, you might want to take a look at this answer for removing duplicate elements from your array. One way is to use a set, which will not preserve order but will ensure elements only occur once.