how prevent dequeueReusableCellWithIdentifier to repeat my segment select? - ios

I have table contains custom cells contain uisegment
The problem is when select any item in uisegment and scroll down in the table view it change the selection in uisegment in the cells down on table
almost cell 1 like 11 and 2 like 12
It related to dequeueReusableCellWithIdentifier and my question is what is the best way to solve it ?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let item = self.cells.items[indexPath.row]
if let cellHedaer: TcPationQuestionHeader = tableView.dequeueReusableCellWithIdentifier("HeaderItem") as? TcPationQuestionHeader {
cellHedaer.setCell(Results.Questions[indexPath.row/2 ])
if item as? SwiftyAccordionCells.HeaderItem != nil {
let cellItem: TcPationQuestionItem = tableView.dequeueReusableCellWithIdentifier("Item") as! TcPationQuestionItem
cellItem.setCell(Results.Questions[indexPath.row/2])
return cellItem
}
cellHedaer.selectionStyle = UITableViewCellSelectionStyle.None
return cellHedaer
}

I see 2 major problems in your code:
You shouldn't keep the state inside UI. Every accessing data from UI
is a big mistake. You should keep selection inside model. The easiest way is to keep array var inside controller.
In some cases you will call dequeueReusableCellWithIdentifier 2 times. That shouldn't happen too.
Don't forget the implementation of cellForRow is connected to numberOfRowsAtIndexPath and numberOfSections. If you want to more detailed help paste here these 2 functions.

Related

Number of Lines for UILabel not Updating as Expected When TableViewCell is Tapped

I am changing the numberOfLines attribute on a label that lives in a custom UITableViewCell when the cell is tapped. However, this is not reflected in the UI until the second tap. The cell is configured as a prototype cell in the table view to initially have 2 lines.
Interestingly enough, when I print out the numberOfLines value before and after my tapped() function runs, the values start off different, and then synchronize - after the first tap, I see 2 lines before the function runs, then 0 lines after the function runs. However, after subsequent taps, I see the same value before and after my function, which makes it seem like it's not doing anything, even though the UI does stretch and shrink the cell, and the numberOfLines value is changed for the next time the didSelectRowAtIndexPath function runs.
I'm only seeing this behavior with tableView.reloadRows(). If I do a full update with tableView.reloadData(), the cell appropriately grows and collapses the first time it is tapped. However, this feels a bit ham-fisted and doesn't animate nicely like reloadRows() does.
TableView Implementation
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ReviewTableViewCell
else { return }
let data = tableData[indexPath.row]
print("old number of lines: \(cell.detailLabel.numberOfLines)")
//data.isOpen is set to false initially
cell.tapped(data.isOpen)
tableData[indexPath.row].isOpen = !data.isOpen
tableView.reloadRows(at: [indexPath], with: .fade)
print("old number of lines: \(cell.detailLabel.numberOfLines)")
// tableView.reloadData()
}
Custom Table View Cell method
func tapped(_ isOpen: Bool) {
if !isOpen {
detailLabel.numberOfLines = 0 }
else {
detailLabel.numberOfLines = 2 }
}
I am expecting this code to expand the cell once it is reloaded with tableView.reloadRows() if the numberOfLines is set to 0 and collapse the cell when it is set to 2. This does work, but only after tapping the cell two+ times. This should work with the first tap as well.
Here is a link of a gif that shows the issue: https://imgur.com/a/qe2uAXj
Here is a sample project that is similar to what's going on in my app: https://github.com/imattice/CellLabelExample
Just to be clear, to get this trick work UILabel generally must be constrained on each side to it's superview, in this way when it changes its intrinsicContentSize is able to push each side to accomodate the text.
Saying that, try to wrap the tapped method with those two methods:
tableView.beginUpdates()
if !isOpen {
detailLabel.numberOfLines = 0
}
else {
detailLabel.numberOfLines = 2
}
tableView.endUpdates()
Of course tableview must be set to automatic size:
tableView.estimatedRowHeight = <#What you want#>
tableView.rowHeight = UITableView.automaticDimension
I was able to work out what was going on. The problem is in two parts.
The first part is calling reloadRows(). This method is swapping out the cells with a new cell rather than updating the cell that already exists. Therefore, I'm changing the number of lines on that hidden swap cell rather than the cell that is in view. This behavior is mentioned in the docs:
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out.
Additionally, I'm using structs as the data model for tracking the open status of the cell. In Swift, structs are copy-on-write, which means that if a value is changed on that struct, a new struct is created rather than changing the value of that struct I'm pointing to. This means the line tableData[indexPath.row].isOpen = !data.isOpen doesn't do anything useful - we look at the tableData struct at the index path, get it's isOpen value, copy a new struct and change that new struct's isOpen value, and then throw it out because the new struct is not assigned anywhere.
The solution is to not use the reloadRows() method and to either use
A) a class for the data object
B) replace the data at indexPath.row to the copied struct
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CustomCell else { return }
var data = tableData[indexPath.row]
tableView.beginUpdates()
cell.tapped(isOpen: data.isOpen)
data.isOpen = !data.isOpen
tableData[indexPath.row] = data
tableView.endUpdates()
}

Showing and hiding a view only on a specific cell of a table view

I have a table view with custom cells. They are quite tall, so only one cell is completely visible on the screen and maybe, depending on the position of that cell, the top 25% of the second one. These cells represent dummy items, which have names. Inside of each cell there is a button. When tapped for the first time, it shows a small UIView inside the cell and adds the item to an array, and being tapped for the second time, hides it and removes the item. The part of adding and removing items works fine, however, there is a problem related to showing and hiding views because of the fact that cells are reused in a UITableView
When I add the view, for example, on the first cell, on the third or fourth cell (after the cell is reused) I can still see that view.
To prevent this I've tried to loop the array of items and check their names against each cell's name label's text. I know that this method is not very efficient (what if there are thousands of them?), but I've tried it anyway.
Here is the simple code for it (checkedItems is the array of items, for which the view should be visible):
if let cell = cell as? ItemTableViewCell {
if cell.itemNameLabel.text != nil {
for item in checkedItems {
if cell.itemNameLabel.text == item.name {
cell.checkedView.isHidden = false
} else {
cell.checkedView.isHidden = true
}
}
}
This code works fine at a first glance, but after digging a bit deeper some issues show up. When I tap on the first cell to show the view, and then I tap on the second one to show the view on it, too, it works fine. However, when I tap, for example, on the first one and the third one, the view on the first cell disappears, but the item is still in the array. I suspect, that the reason is still the fact of cells being reused because, again, cells are quite big in their height so the first cell is not visible when the third one is. I've tried to use the code above inside tableView(_:,cellForRow:) and tableView(_:,willDisplay:,forRowAt:) methods but the result is the same.
So, here is the problem: I need to find an EFFICIENT way to check cells and show the view ONLY inside of those which items are in the checkedItems array.
EDITED
Here is how the cell looks with and without the view (the purple circle is the button, and the view is the orange one)
And here is the code for the button:
protocol ItemTableViewCellDelegate: class {
func cellCheckButtonDidTapped(cell: ExampleTableViewCell)
}
Inside the cell:
#IBAction func checkButtonTapped(_ sender: UIButton) {
delegate?.cellCheckButtonDidTapped(cell: self)
}
Inside the view controller (NOTE: the code here just shows and hides the view. The purpose of the code is to show how the button interacts with the table view):
extension ItemCellsTableViewController: ItemTableViewCellDelegate {
func cellCheckButtonDidTapped(cell: ItemTableViewCell) {
UIView.transition(with: cell.checkedView, duration: 0.1, options: .transitionCrossDissolve, animations: {
cell.checkedView.isHidden = !cell.checkedView.isHidden
}, completion: nil)
}
EDITED 2
Here is the full code of tableView(_ cellForRowAt:) method (I've deleted the looping part from the question to make it clear what was the method initially doing). The item property on the cell just sets the name of the item (itemNameLabel's text).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
ItemTableViewCell.identifier, for: indexPath) as? ItemTableViewCell{
cell.item = items[indexPath.row]
cell.delegate = self
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
I've tried the solution, suggested here, but this doesn't work for me.
If you have faced with such a problem and know how to solve it, I would appreciate your help and suggestions very much.
Try this.
Define Globally : var arrIndexPaths = NSMutableArray()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = self.tblVW.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.textLabel?.text = String.init(format: "Row %d", indexPath.row)
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(btnTapped), for: .touchUpInside)
if arrIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.red.withAlphaComponent(0.2)
}
else {
cell.backgroundColor = UIColor.white
}
return cell;
}
#IBAction func btnTapped(_ sender: UIButton) {
let selectedIndexPath = NSIndexPath.init(row: sender.tag, section: 0)
// IF YOU WANT TO SHOW SINGLE SELECTED VIEW AT A TIME THAN TRY THIS
arrIndexPaths.removeAllObjects()
arrIndexPaths.add(selectedIndexPath)
self.tblVW.reloadData()
}
I would keep the state of your individual cells as part of the modeldata that lies behind every cell.
I assume that you have an array of model objects that you use when populating you tableview in tableView(_:,cellForRow:). That model is populated from some backend service that gives you some JSON, which you then map to model objects once the view is loaded the first time.
If you add a property to your model objects indicating whether the cell has been pressed or not, you can use that when you populate your cell.
You should probably create a "wrapper object" containing your original JSON data and then a variable containing the state, lets call it isHidden. You can either use a Bool value or you can use an enum if you're up for it. Here is an example using just a Bool
struct MyWrappedModel {
var yourJSONDataHere: YourModelType
var isHidden = true
init(yourJSONModel: YourModelType) {
self.yourJSONDataHere = yourJSONModel
}
}
In any case, when your cell is tapped (in didSelectRow) you would:
find the right MyWrappedModel object in your array of wrapped modeldata objects based on the indexpath
toggle the isHidden value on that
reload your affected row in the table view with reloadRows(at:with:)
In tableView(_:,cellForRow:) you can now check if isHidden and do some rendering based on that:
...//fetch the modelObject for the current IndexPath
cell.checkedView.isHidden = modelObject.isHidden
Futhermore, know that the method prepareForReuse exists on a UITableViewCell. This method is called when ever a cell is just about to be recycled. That means that you can use that as a last resort to "initialize" your table view cells before they are rendered. So in your case you could hide the checkedView as a default.
If you do this, you no longer have to use an array to keep track of which cells have been tapped. The modeldata it self knows what state it holds and is completely independent of cell positions and recycling.
Hope this helps.

Compare two labels from different rows in swift

say me, please, how better compare two same (label has name "labelNumber") labels from different rows in Tableview
For example: I know, that in row №0 label is "06" (Int) and in next cell (row №1) this label is "07". So, "07" > "06". How compare it with swift language?
Thanks!
The wrong way
As stated by #vadian, you should NOT use the UI you populated to perform calculations on data.
However this is the code to compare the values inside 2 UITableViewCell(s)
class Controller: UITableViewController {
func equals(indexA: NSIndexPath, indexB: NSIndexPath) -> Bool? {
guard let
cellA = tableView.cellForRowAtIndexPath(indexA),
cellB = tableView.cellForRowAtIndexPath(indexB) else { return nil }
return cellA.textLabel?.text == cellB.textLabel?.text
}
}
The right way
Think about how you are populating the cells. I imagine you are using some Model Value. So just compare the Model Values you are using to populate the cells.
Probably something like this
class Controller: UITableViewController {
var data: [Int] = [] // ....
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCellWithIdentifier("YourCellID") else { fatalError("No cell found with this id: 'YourCellID'")}
cell.textLabel?.text = String(data[indexPath.row])
return cell
}
func equals(indexA: NSIndexPath, indexB: NSIndexPath) -> Bool? {
return data[indexA.row] == data[indexB.row]
}
}
Compare the values stored within your data array:
if dataArray[0].myIntegerValue > dataArray[1].myIntegerValue {
// Do your stuff
}
Edit: this assumes your data is stored as objects with that Int as an attribute.
As the others have said, don't do that. In the MVC design pattern, labels are views. They are for displaying data, not storing it.
Trying to read values from table view labels is especially bad, because as the table view scrolls, the cells that go off-screen will be recycled and the values in their views will be discarded. You need to save your data to a model object. (An array works just fine to save table view data, or an array of arrays for a sectioned table view.)

Custom Cells Not Populating Correct Data When Off-Screen

Forgive the naive nature of this question, but despite my searching here, I can't quite find a solution.
In my project, I have a UITableView with custom cells, all of which are populated by data from an API. At this point, based on the desired design, four of the cells are visible on-screen at launch, and I have to scroll to see the other four. Each cell has two components; a label and a UIView, which is generating a line chart. The first four cells all work properly, but the second set of cells (which are off-screen at loading) populate the label correct, but the UIView is not showing the correct set of data (it is populating with previously used data).
Oddly, if I tap on one of the problematic cells, the cell immediately reloads with the correct data.
For context, here's part of my TableViewController;
...
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return minions.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return minionCellAtIndexPath(indexPath)
}
func minionCellAtIndexPath(indexPath:NSIndexPath) -> MinionCell {
let minion = minions[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier(minionCellIdentifier, forIndexPath: indexPath) as! MinionCell
if let name = minion.name {
cell.nameLabel?.text = minion.name ?? "Minion"
cell.lineChart?.graphPoints = minion.bandwidth ?? [1,2,3,4,5]
} else {
cell.nameLabel?.text = "No data available."
cell.lineChart?.graphPoints = [1,2,3,4,5]
}
return cell
}
...
Is it possible that I need to somehow "pre-render" the off-screen cells at load? Again, sorry for the naive nature.
As always, thank you for your time!
As you most probably know, cells that go off screen get reused for the new rows. That is why the new rows come with the old graphs. The fact that if you tap the cell the graph refreshes and is correctly displayed makes me think that it is a drawing problem. Try triggering a force redraw by calling setNeedsDisplay on the view with the graph:
cell.lineChart?.graphPoints = [1,2,3,4,5]
cell.lineChart?.setNeedsDisplay()
I am somewhat skeptical about your function minionCellAtIndexPath.
The reason you are getting older data again, and not newer is probably because you are not resetting it.
Before your if let checks, set hardcoded values, the ones that you are setting in your else. and then run again.

UITableViewCells not displaying first time

I'm trying to create an autocompleter using iOS 8, Swift and Xcode 6.3
I have a problem that I'm trying to solve, but I gave up... I hope someone can help here. The problem is that (custom) UITableViewCell's are not displaying when the initial dataSource is empty. When adding data to datasource and reloading the tableView, the cells SHOULD display, but they don't... At least, the first time they don't... A second time, they DO... When I initialize the table with non-empty data, the problem doesn't occur. I guess something goes wrong with dequeueReusableCellWithIdentifier. In beginning, no reusable cells are found, or something. But I don't know why...
Relevant code, in ViewController.swift:
// filteredWords is a [String] with zero or more items
#IBAction func editingChanged(sender: UITextField) {
autocompleteTableView.hidden = sender.text.isEmpty
filteredWords = dataManager.getFilteredWords(sender.text)
refreshUI()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! AutocompleteTableViewCell
cell.title.text = filteredWords[indexPath.row]
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredWords.count
}
func refreshUI() {
self.autocompleteTableView.reloadData()
}
I created a sample project on github:
https://github.com/dirkpostma/swift-autocomplete
And a movie on YoutTube to show what goes wrong:
https://www.youtube.com/watch?v=ByMsy4AaHYI
Can anyone look at it and spot the bug...?
Thanks in advance!
You've accidentally hidden your cell.
Open Main.storyboard
Select Cell
Uncheck Hidden
Side note: As for why it's displaying the second time around with the cell hidden? It appears to be a bug. It should still be hidden (print cell.hidden, notice it's always true despite showing the text on the screen).
I think you need to change your code. Check out below code. It is because if you remember in Objective C you needed to check if the Cell was nil and then initialise it. The reuse identifier is usually reusing an already created cell, but on the first launch this does not work because there is no Cell to use. Your current code assumes always that the cell is created (re-used) because you are using ! in the declaration, so if you use the optional (?) it can be null and you then can create the cell
var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as? AutocompleteTableViewCell
if cell == nil
{
//You should replace this with your initialisation of custom cell
cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "CELL")
}
cell.title.text = filteredWords[indexPath.row]
return cell

Resources