When I select a cell in an UITableView I change its image, but a cell out of sight (need to scroll down to see the affected cell) gets affected which means that the affected cell also change its image. It must be something about the reused cells, but I can't figured out why and how to solve this problem. Hope you guys can help me, thank you.
Another thing is, the cell should not reset its image when it get scroll out of sight.
Here is my delegates for the UITableView:
//UITableview delegate
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Arecipe.IngredientArr.count
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if (tableView == IngrediensTableView) {
print("Cell")
var cell = tableView.dequeueReusableCellWithIdentifier("Cell")
let ingredient = self.Arecipe.IngredientArr[indexPath.row]
if ((cell == nil)) {
cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "Cell")
cell!.imageView?.image = UIImage(named: "Ingr_Uncheck")
}
cell!.textLabel?.text = ingredient.UnitValue + " " + ingredient.UnitName + " " + ingredient.Name
cell!.selectionStyle = .None
return cell!
}
return UITableViewCell()
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("Row \(indexPath.row) selected")
if (tableView == IngrediensTableView) {
let cell: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
print(cell.textLabel?.text)
if(cell.imageView?.image == UIImage(named: "Ingr_Uncheck")) {
cell.imageView?.image = UIImage(named: "Ingr_Check")
} else {
cell.imageView?.image = UIImage(named: "Ingr_Uncheck")
}
}
}
the affected cell also change its image. It must be something about the reused cells
That's right! The reason is that you set Ingr_Uncheck image only to a brand-new cell. If a cell gets reused, you skip setting the image, keeping whatever was set there previously.
The decision to set Ingr_Check vs. Ingr_Uncheck image needs to be done based on the state of your table's model.
if(myCodeThatChecksIfRowIsChecked(indexPath.row)) {
cell!.imageView?.image = UIImage(named: "Ingr_Check")
} else {
cell!.imageView?.image = UIImage(named: "Ingr_Uncheck")
}
Now the proper image is set to both new and reused cells, making sure that reusing a previously checked cell does not change the visuals.
The code myCodeThatChecksIfRowIsChecked needs to rely on some stored state that you change inside your didSelectRowAtIndexPath method. This is where you need to maintain a list of row numbers for cells that have been checked. This is also the list that you consult to decide if a cell should be unchecked or not: doing it based on the image is not the correct approach.
u cant compare 2 uiimage by "=="
if(cell.imageView?.image == UIImage(named: "Ingr_Uncheck"))
use this
if( [UIImagePNGRepresentation(cell.imageView?.image) isEqual:UIImagePNGRepresentation(UIImage(named: "Ingr_Uncheck"))] == YES)
You could resolve this in two ways. Either set the image to nil cellForRow
cell.imageView?.image = nil
Or if you have custom class for the cell, implement
override func prepareForReuse() {
imageView?.image = nil
}
Reset with nil or with the default image
Related
I'm using a unclickable tableView to display different information of one object.
For this informations I have different custom cell types one where I placed a map, if my object have locations, one have a list with links, and another a multiple line label for a little description...for example.
I manage this cells with:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! MapCell
return cell
} else if indexPath.row == 1 {
let cell: textCell = tableView.dequeueReusableCellWithIdentifier("textCell") as! TextCell
return cell
} else if indexPath.row == 2 {
let cell: listCell = tableView.dequeueReusableCellWithIdentifier("listCell") as! ListCell
return cell
}
}
So far so good, everything working fine. My problem is, not every object needs a map, some of them just need some text and a list, other objects need a map and a list, other all of them. I want my tableView to skip some cells if there is a condition.
I know, I can make an symbolic array for changing the number of cells of my tableView, but that deleting just from the end of my tableView, not specific cells.
One of my ideas is to generate a empty cell, maybe with a height of 0 or 1 so that I can do something like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
if mapCellNeeded {
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! mapCell
} else {
let cell: emptyCell = tableView.dequeueReusableCellWithIdentifier("emptyCell") as! EmptyCell
}
return cell
} else if indexPath.row == 1 {
...
}...
}
put I don't know if there isn't an efficient way. Hope you guys can help me.
Your solution would work. Another approach (very nice and swifty) would be not to hardcode row numbers, but rather use enum instead:
enum InfoCellType {
case Map
case Text
case Links
}
...
var rows = [InfoCellType]()
...
// when you know what should be there or not
func constructRows() {
if (mapCellNeeded) {
rows.append(InfoCellType.Map)
}
rows.append(InfoCellType.Text)
... etc
}
Then in the table view methods just see what's the type for current indexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellType: InfoCellType = self.rows[indexPath.row]
switch cellType {
case .Map:
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! mapCell
return cell
case .Text:
...
case.Links:
...
}
}
This solution also allows to easily change order of rows - just change the order of items in rows array.
I have a tableViewCell that uses a checkmark in accessoryType of cell. I have a function that puts the contents of the cell into textField and similarly removes the text from the text field when it is unchecked.
It seems to work fine but if I check a cell and want to check a cell thats not visible (IOW) I need to scroll the tableView, the cell that was checked (is now not visible) seems to uncheck itself (Only when I check a new visible cell).
The multi select works with visible cells only.
Here is my code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier, forIndexPath: indexPath)
let row = indexPath.row
cell.textLabel?.text = painArea[row]
cell.accessoryType = .None
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//selectedRow = indexPath
tableView.deselectRowAtIndexPath(indexPath, animated: true)
let row = indexPath.row
let cell = tableView.cellForRowAtIndexPath(indexPath)
if cell!.accessoryType == .Checkmark {
cell!.accessoryType = .None
} else {
cell!.accessoryType = .Checkmark
}
populateDescription()
print(painArea[row])
}
var painDescription = ["very sore"]
func populateDescription() {
painDescription.removeAll()
severityText.text = ""
for cell in tableView.visibleCells {
if cell.accessoryType == .Checkmark {
painDescription.append((cell.textLabel?.text)! + " ")
}
var painArea = ""
var i = 1
while i <= painDescription.count {
painArea += painDescription[i-1] + "~"
i = i + 1
}
severityText.text = painArea
}
I hope I am explaining myself adequately. I don't want the non visible cells to be unchecked and thus removed from my text field unless I uncheck it.
Any ideas would be most appreciated.
Kind regards
Wayne
It is happing because of reusability of Cell. Instead of setting Checkmark in didSelect try to set in the cellForRowAtIndexPath. Also you need to create model class like this to solve your problem
class ModelClass: NSObject {
var isSelected: Bool = false
//Declare other property that you are using cellForRowAtIndexPath
}
Now check this isSelected in cellForRowAtIndexPath like below.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier, forIndexPath: indexPath)
let row = indexPath.row
let modelClass = painArea[row]
cell.textLabel?.text = modelClass.name
if modelClass.isSelected {
cell.accessoryType = .Checkmark
}
else {
cell.accessoryType = .None
}
return cell
}
Now change your didSelect like this
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var modelClass = painArea[indexPath.row]
modelClass.isSelected = !modelClass.isSelected
self.tableView.reloadData()
populateDescription()
print(painArea[row])
}
Hope this will help you.
It's because whenever u scroll the cell out of view and scroll back, it will call the cellForRow again and set everything back to default, what you have to do is create a properly dataSource, whenever a cell got checked, you update the dataSource with a Bool indicate it has been checked, then set it back in the cellForRow or cellWillDisplay
I have a problem which i'm not sure how to solve.
I have two custom cell nibs - data for both is fetched from separate arrays.
The structure is the following
nib1-cell line1
nib1-cell line2
...
nib1-cell line n
nib2-cell line1
there is always one nib2-cell at the end with the uibutton.
Once the uibutton is pressed - the nib1 array is appended.
I figured out a way how to insert values at the bottom of the tableview, but when i scroll downwards or upwards the cell with nib2 is reused and replaced with nib1-cell data.
How can i either prevent those cells from being reused or save their state ?
Thank you.
UPDATE: datasource and cellForRowAtIndexPath code
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return someTagsArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.row < someTagsArray.count - 1){
var cell:TblCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! TblCell
cell.lblCarName.text = someTagsArray[indexPath.row]
return cell
} else if (indexPath.row == someTagsArray.count - 1){
var celle:vwAnswers = self.tableView.dequeueReusableCellWithIdentifier("cell2") as! vwAnswers
celle.Answer1.setTitle(answersdict[answersdict.endIndex - 2], forState:UIControlState.Normal)
answertitle1 = "\(celle.Answer1.currentTitle!)"
celle.Answer2.setTitle(answersdict.last, forState:UIControlState.Normal)
answertitle2 = "\(celle.Answer2.currentTitle!)"
//println(answertitle2)
return celle
} else {
var cell2:TblCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! TblCell
return cell2
}
}
You have to determine which type of cell you want in cellForRowAtIndexPath and dequeue the correct reusable cell. Maybe something like if (indexPath.row + 1)%3 == 0 then dequeue an answer cell.
However, you may possibly want to look in to using a section header for this instead. Hard to say without seeing how you implement your data source.
I have been struggling with this issue. I can scroll freely between the tag cells because it actually remembers them. But if I get the description cell out of my view it immediately removes it from memory and doesn't get it back. Instead I just get "fatal error: unexpectedly found nil while unwrapping an Optional value" when I scroll back to the description. So I have the following pieces of code:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44.0
tableView.reloadData()
}
I don't know if the viewWillAppear is of any importance in this case but if it is then tell me. Anyway, this is for filling in the cells in my table view:
func GetDescription(cell:descCell, indexPath: NSIndexPath) {
cell.descText.text = descriptTextTwo.htmlToString
}
func GetTagCell(cell:basicTag, indexPath: NSIndexPath) {
let item = tagResults[indexPath.row]!
cell.titleLabel.text = item["tagname"]?.htmlToString
}
func GetValueCell(cell: basicTag, indexPath: NSIndexPath) {
let item = tagResults[indexPath.row]!
cell.valueLabel.text = item["value"]?.htmlToString
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if filledDescription == false {
return getDescriptionAtIndexPath(indexPath)
} else {
return getTagAtIndexPath(indexPath)
}
}
func getDescriptionAtIndexPath(indexPath:NSIndexPath) -> descCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier(descriptionCell) as descCell
GetDescription(cell, indexPath: indexPath)
filledDescription = true
return cell
}
func getTagAtIndexPath(indexPath: NSIndexPath) -> basicTag {
let cell = self.tableView.dequeueReusableCellWithIdentifier(tagCell) as basicTag
GetTagCell(cell, indexPath: indexPath)
GetValueCell(cell, indexPath: indexPath)
return cell
}
So how can I make Swift remember what is in the first cell? Because I am guessing that that is what happens, that it removes what was in the first cell as soon as you get it out of the view. I am guessing I have to do something with "indexPath" but I am not exactly sure how to implement it in this case and if I am far off, please tell me what I am doing wrong. Thanks!
Change the following :
if filledDescription == false {
return getDescriptionAtIndexPath(indexPath)
} else {
return getTagAtIndexPath(indexPath)
}
With:
if indexPath.row == 0 {
return getDescriptionAtIndexPath(indexPath)
} else {
return getTagAtIndexPath(indexPath)
}
This will make sure that the first cell in the table will always treated as a "Description" cell. Since the filledDescription never becomes false after your set it to true, when you get back to the first cell it is treated as a "Tag" cell (due to the if line) where in fact the reusable cell contains "Description" cell data
I have been trying to implement a feature in my app so that when a user taps a cell in my table view, the cell expands downwards to reveal notes. I have found plenty of examples of this in Objective-C but I am yet to find any for Swift.
This example seems perfect: Accordion table cell - How to dynamically expand/contract uitableviewcell?
I had an attempt at translating it to Swift:
var selectedRowIndex = NSIndexPath()
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedRowIndex = indexPath
tableView.beginUpdates()
tableView.endUpdates()
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if selectedRowIndex == selectedRowIndex.row && indexPath.row == selectedRowIndex.row {
return 100
}
return 70
}
However this just seems to crash the app.
Any ideas?
Edit:
Here is my cellForRowAtIndexPath code:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:CustomTransactionTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as CustomTransactionTableViewCell
cell.selectionStyle = UITableViewCellSelectionStyle.None
if tableView == self.searchDisplayController?.searchResultsTableView {
cell.paymentNameLabel.text = (searchResults.objectAtIndex(indexPath.row)) as? String
//println(searchResults.objectAtIndex(indexPath.row))
var indexValue = names.indexOfObject(searchResults.objectAtIndex(indexPath.row))
cell.costLabel.text = (values.objectAtIndex(indexValue)) as? String
cell.dateLabel.text = (dates.objectAtIndex(indexValue)) as? String
if images.objectAtIndex(indexValue) as NSObject == 0 {
cell.paymentArrowImage.hidden = false
cell.creditArrowImage.hidden = true
} else if images.objectAtIndex(indexValue) as NSObject == 1 {
cell.creditArrowImage.hidden = false
cell.paymentArrowImage.hidden = true
}
} else {
cell.paymentNameLabel.text = (names.objectAtIndex(indexPath.row)) as? String
cell.costLabel.text = (values.objectAtIndex(indexPath.row)) as? String
cell.dateLabel.text = (dates.objectAtIndex(indexPath.row)) as? String
if images.objectAtIndex(indexPath.row) as NSObject == 0 {
cell.paymentArrowImage.hidden = false
cell.creditArrowImage.hidden = true
} else if images.objectAtIndex(indexPath.row) as NSObject == 1 {
cell.creditArrowImage.hidden = false
cell.paymentArrowImage.hidden = true
}
}
return cell
}
Here are the outlet settings:
It took me quite a lot of hours to get this to work. Below is how I solved it.
PS: the problem with #rdelmar's code is that he assumes you only have one section in your table, so he's only comparing the indexPath.row. If you have more than one section (or if you want to already account for expanding the code later) you should compare the whole index, like so:
1) You need a variable to tell which row is selected. I see you already did that, but you'll need to return the variable to a consistent "nothing selected" state (for when the user closes all cells). I believe the best way to do this is via an optional:
var selectedIndexPath: NSIndexPath? = nil
2) You need to identify when the user selects a cell. didSelectRowAtIndexPath is the obvious choice. You need to account for three possible outcomes:
the user is tapping on a cell and another cell is expanded
the user is tapping on a cell and no cell is expanded
the user is tapping on a cell that is already expanded
For each case we check if the selectedIndexPath is equal to nil (no cell expanded), equal to the indexPath of the tapped row (same cell already expanded) or different from the indexPath (another cell is expanded). We adjust the selectedIndexPath accordingly. This variable will be used to check the right rowHeight for each row. You mentioned in comments that didSelectRowAtIndexPath "didn't seem to be called". Are you using a println() and checking the console to see if it was called? I included one in the code below.
PS: this doesn't work using tableView.rowHeight because, apparently, rowHeight is checked only once by Swift before updating ALL rows in the tableView.
Last but not least, I use reloadRowsAtIndexPath to reload only the needed rows. But, also, because I know it will redraw the table, relayout when necessary and even animate the changes. Note the [indexPath] is between brackets because this method asks for an Array of NSIndexPath:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
println("didSelectRowAtIndexPath was called")
var cell = tableView.cellForRowAtIndexPath(indexPath) as! MyCustomTableViewCell
switch selectedIndexPath {
case nil:
selectedIndexPath = indexPath
default:
if selectedIndexPath! == indexPath {
selectedIndexPath = nil
} else {
selectedIndexPath = indexPath
}
}
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}
3) Third and final step, Swift needs to know when to pass each value to the cell height. We do a similar check here, with if/else. I know you can made the code much shorter, but I'm typing everything out so other people can understand it easily, too:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let smallHeight: CGFloat = 70.0
let expandedHeight: CGFloat = 100.0
let ip = indexPath
if selectedIndexPath != nil {
if ip == selectedIndexPath! {
return expandedHeight
} else {
return smallHeight
}
} else {
return smallHeight
}
}
Now, some notes on your code which might be the cause of your problems, if the above doesn't solve it:
var cell:CustomTransactionTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as CustomTransactionTableViewCell
I don't know if that's the problem, but self shouldn't be necessary, since you're probably putting this code in your (Custom)TableViewController. Also, instead of specifying your variable type, you can trust Swift's inference if you correctly force-cast the cell from the dequeue. That force casting is the as! in the code below:
var cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier" forIndexPath: indexPath) as! CustomTransactionTableViewCell
However, you ABSOLUTELY need to set that identifier. Go to your storyboard, select the tableView that has the cell you need, for the subclass of TableViewCell you need (probably CustomTransactionTableViewCell, in your case). Now select the cell in the TableView (check that you selected the right element. It's best to open the document outline via Editor > Show Document Outline). With the cell selected, go to the Attributes Inspector on the right and type in the Identifier name.
You can also try commenting out the cell.selectionStyle = UITableViewCellSelectionStyle.None to check if that's blocking the selection in any way (this way the cells will change color when tapped if they become selected).
Good Luck, mate.
The first comparison in your if statement can never be true because you're comparing an indexPath to an integer. You should also initialize the selectedRowIndex variable with a row value that can't be in the table, like -1, so nothing will be expanded when the table first loads.
var selectedRowIndex: NSIndexPath = NSIndexPath(forRow: -1, inSection: 0)
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row == selectedRowIndex.row {
return 100
}
return 70
}
Swift 4.2 var selectedRowIndex: NSIndexPath = NSIndexPath(row: -1, section: 0)
I suggest solving this with modyfing height layout constraint
class ExpandableCell: UITableViewCell {
#IBOutlet weak var img: UIImageView!
#IBOutlet weak var imgHeightConstraint: NSLayoutConstraint!
var isExpanded:Bool = false
{
didSet
{
if !isExpanded {
self.imgHeightConstraint.constant = 0.0
} else {
self.imgHeightConstraint.constant = 128.0
}
}
}
}
Then, inside ViewController:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.estimatedRowHeight = 2.0
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.tableFooterView = UIView()
}
// TableView DataSource methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:ExpandableCell = tableView.dequeueReusableCell(withIdentifier: "ExpandableCell") as! ExpandableCell
cell.img.image = UIImage(named: indexPath.row.description)
cell.isExpanded = false
return cell
}
// TableView Delegate methods
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell
else { return }
UIView.animate(withDuration: 0.3, animations: {
tableView.beginUpdates()
cell.isExpanded = !cell.isExpanded
tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.top, animated: true)
tableView.endUpdates()
})
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell
else { return }
UIView.animate(withDuration: 0.3, animations: {
tableView.beginUpdates()
cell.isExpanded = false
tableView.endUpdates()
})
}
}
Full tutorial available here
A different approach would be to push a new view controller within the navigation stack and use the transition for the expanding effect. The benefits would be SoC (separation of concerns). Example Swift 2.0 projects for both patterns.
https://github.com/justinmfischer/SwiftyExpandingCells
https://github.com/justinmfischer/SwiftyAccordionCells
After getting the index path in didSelectRowAtIndexPath just reload the cell with following method
reloadCellsAtIndexpath
and in heightForRowAtIndexPathMethod check following condition
if selectedIndexPath != nil && selectedIndexPath == indexPath {
return yourExpandedCellHieght
}