Add/Remove UITableView Selection to Array, Checkmarks Disappear upon scroll - Swift - ios

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.

Related

In Swift, I'd like the last 3 sections have only labels in their rows (and NOT text fields)

In my Swift project I have a table controller view with a table view inside. The table view is divided into 4 section and every section has 4 rows. Currently, each row is formed by a label beside a text field.
My purpose is that only rows in the first section has label beside text field. On the contrary, I want the last 3 sections have only labels in their rows (and NOT text fields).
Please, help me.
That's the code I wrote to manage with this problem but it's not working:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print("index ", indexPath.section);
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath)
cell.textLabel?.text = self.items[indexPath.section][indexPath.row];
if(indexPath.section == 0){
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell") as! TextInputTableViewCell
cell.configure("", placeholder: "name")
}
return cell
}
It's not working because you are using the same cell for all the rows. You need to define two different rows. You can do this by setting prototype cells to more than one row (two in your case).
Each cell must have its own reuse identifier and it must be unique within that table view.
Then in your tableView(cellForRowAtIndexPath:) you can ask:
if indexPath.section == 0 {
cell = tableView.dequeueReusableCellWithIdentifier("firstSectionCell", forIndexPath: indexPath)
//
} else {
cell = tableView.dequeueReusableCellWithIdentifier("otherSectionCell", forIndexPath: indexPath)
//
}
Also note that in Swift you do not need to use parenthesis in if-statement (nor for, while, etc). So I suggest you remove them as they are pointless.
It looks like your cell in if(indexPath.section == 0) block doesn't actually have scope outside that block so any properties set there won't get returned there. If you just want to remove the textField, but keep the label, You can just set the textField.hidden = true. Here is how I would go about it.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath) as! TextInputTableViewCell
cell.textLabel?.text = self.items[indexPath.section][indexPath.row];
if(indexPath.section == 0){
cell.textField.hidden = false //Assumes TextInputTableViewCell's textField property is called "textField"
cell.configure("", placeholder: "name")
} else {
cell.textField.hidden = true //Not in first section we will hide textField
}
return cell
}
Doing it this way you can use the same cell class for every cell in your tableView, but just hide what you want based on its section.

UITableViewCell selection challenge

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.

reloadRowsAtIndexPaths doesn't update my cell data

I have a UITableView in my ViewController.
One of the cell could be tap into another TableViewController to allow select a value.
I want to update my cell after back from the callee ViewController.
right now, i could pass back the selected value by delegate.
However, i tried following way, none of them works.
self.mainTable.reloadData()
dispatch_async(dispatch_get_main_queue()) {
self.mainTable.reloadData()
}
self.mainTable.beginUpdates()
self.mainTable.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
self.mainTable.endUpdates()
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
was called and executed without error.
but the UI just doesn't change
here is the way I update value in cellForRowAtIndexPath
if let currentCell = tableView.cellForRowAtIndexPath(indexPath) as UITableViewCell! {
currentCell.textLabel?.text = address
return currentCell
}
Here is my cellForRowAtIndexPath -
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let id = "Cell"
println(indexPath)
if indexPath.row == 1 {
var cell = tableView.dequeueReusableCellWithIdentifier(id) as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: id)
cell?.contentMode = UIViewContentMode.Center
cell?.selectionStyle = UITableViewCellSelectionStyle.None
cell?.contentView.addSubview(mapView!)
}
return cell!
}else{
let cell = UITableViewCell()
cell.textLabel?.text = self.address
return cell
}
}
Here is the delegate method -
func passBackSelectedAddress(address: String) {
self.address = address
var indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.mainTable.beginUpdates()
self.mainTable.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
self.mainTable.endUpdates()
}
My fix:
After more debug, i find the cause,
the self.address value is updated in delegate, however it roll back to previous value in cellForRowAtIndexPath.
I change the property to a static property, then resolve the problem.
I'm not sure what's wrong with instance property, and why it reverses back.
static var _address:String = ""
It seems like you're trying to grab a cell from the UITableView and then update the textLabel value that way. However, UITableView and UITableViewCell are not meant to be updated in this way. Instead, store the value of address in your class and update this value when the delegate calls back into your class. If cellForRowAtIndexPath constructs the UITableViewCell with the value of self.address, calling mainTable.reloadData() after should update the cell to the new value.
For example:
var address: String
func delegateCompleted(address: String) {
self.address = address
self.mainTable.reloadData()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(<your identifier>)
if (indexPath == <your address cell indexPath>) {
let textLabel = <get your textLabel from the cell>
textLabel?.text = self.address
}
return cell
}
Your cellForRowAtIndexPath has some problems -
You are using the same re-use identifier for different types of cell (one with a map, one without)
When you allocate the table view cell for the other row, you don't include the re-use identifier.
You have no way of referring to the map view that you are adding after the method exits because you don't keep a reference.
If you are using a storyboard then you should create the appropriate prototype cells and subclass(es) and assign the relevant cell reuse ids. If you aren't then I suggest you create a cell subclass and register the classes against the reuse identifiers. Your cellForRowAtIndexPath will then look something like -
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var returnCell:UITableViewCell
if indexPath.row == 1 {
var myMapCell = tableView.dequeueReusableCellWithIdentifier("mapCell", forIndexPath:indexPath) as MYMapCell
myMapCell.contentMode = UIViewContentMode.Center
myMapCell.selectionStyle = UITableViewCellSelectionStyle.None
// Set the properties for a map view in the cell rather than assigning adding an existing map view
returnCell=myMapCell
}else{
returnCell = tableView.dequeueReusableCellWithIdentifier("addressCell", forIndexPath:indexPath)
returnCell.textLabel?.text = self.address
}
return returnCell;
}

Find out indexPath of cell

I have a few UITableViewCells with the labels Test1, Test2, Test3, Test4 and Test5. How can I find out the indexPath of the cell which displays Test3? I don't wanna touch the cell or do something like this. Thanks for your answers!
The cells:
Core Data model:
Create a class variable reference to the cell, and then in cellForRow assign the cell with the "Test3" label to your class variable.
var test3Cell: UITableViewCell?
// ...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = ... // Declaration here
// set up the cell
if cell.textLabel?.text == "Test3" {
self.test3Cell = cell
}
return cell
}
Doing it at the cell level seems backwards — your data model (the table’s UITableViewDataSource) would seem much easier to query.
If you have a reference to the label you can use indexPathForRowAtPoint.
let pointInTable = sender.convertPoint(theLabel.bounds.origin, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(pointInTable)
indexPath where indexPath is an optional.
How are you populating the table? If you are populating the label in the cell of tableview using the content from an NSArray.. you can find the indexpath from the index of the item in the Array obj..
If not, the way i can think of is to loop through the table cells in the tableView
The table view responds to the below methods
func numberOfSections() -> Int
func numberOfRowsInSection(section: Int) -> Int
you can create an NSIndexPath instance using the above information for each row .. NSIndexPath(forRow: , inSection: )
get the UITableviewCell instance using
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
then you can check the text of label in the cell to see if it matches what you need.
This method will check your current visible cells within the tableview and iterate through them looking for the text.
// Swift 1.2
// Store current visable cells in an array
let currentCells = tableView.visibleCells() as Array
// Iterate through cells looking for a match
for cell in currentCells {
println (cell.textLabel?!.text)
if cell.textLabel?!.text == "Test 3" {
println("Test 3 Found")
}
}
// Swift 2.0
// Store current visable cells in an array
let currentCells = tableView.visibleCells
// Iterate through cells looking for a match
for cell in currentCells where cell.textLabel?.text == "Test 3" {
print("Test 3 found")
}

How can I fix crash when tap to select row after scrolling the tableview?

I have a table view like this:
when the user tap one row, I want uncheck the last row and check the selected row. So I wrote my code like this:
(for example my lastselected = 0)
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var lastIndexPath:NSIndexPath = NSIndexPath(forRow: lastSelected, inSection: 0)
var lastCell = self.diceFaceTable.cellForRowAtIndexPath(lastIndexPath) as! TableViewCell
var cell = self.diceFaceTable.cellForRowAtIndexPath(indexPath) as! TableViewCell
lastCell.checkImg.image = UIImage(named: "uncheck")
cell.checkImg.image = UIImage(named: "check")
lastSelected = indexPath.row
}
every thing working fine when I tap a row without scrolling. I realize that when I run the code and scrolling the table immediately and selected the one row. My program will crash with error:
"fatal error: unexpectedly found nil while unwrapping an Optional value"
the error show in this line:
I don't know what wrong in here?
Because you are using reusable cells when you try to select a cell that is not in the screen anymore the app will crash as the cell is no long exist in memory, try this:
if let lastCell = self.diceFaceTable.cellForRowAtIndexPath(lastIndexPath) as! TableViewCell{
lastCell.checkImg.image = UIImage(named: "uncheck")
}
//update the data set from the table view with the change in the icon
//for the old and the new cell
This code will update the check box if the cell is currently in the screen. If it is not currently on the screen when you get the cell to reused (dequeuereusablecellwithidentifier) you should set it properly before display. To do so you will need to update the data set of the table view to contain the change.
Better approach will be storing whole indexPath. not only the row. Try once i think this will work. I had the same problem in one of my app.
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var lastCell = self.diceFaceTable.cellForRowAtIndexPath(lastSelectedIndexPath) as! TableViewCell
var cell = self.diceFaceTable.cellForRowAtIndexPath(indexPath) as! TableViewCell
lastCell.checkImg.image = UIImage(named: "uncheck")
cell.checkImg.image = UIImage(named: "check")
lastSelectedIndexPath = indexPath
}
EDIT: or you can try this.
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
var lastCell = self.diceFaceTable.cellForRowAtIndexPath(indexPath) as! TableViewCell
lastCell.checkImg.image = UIImage(named: "uncheck")
}

Resources