UISegmentControl with UITableView - UITableViewCell layout remains same on changing the segment - ios

In the scenario mentioned in the question title, on changing the segment, ideally the UITableView should reload and hence the UITableViewCell should also reload. The issue is, all the content gets updated like label texts. But if I have expanded a subview of cell in one segment, it remains still expanded after segment is changed.
Segment index change function :
#IBAction func segmentOnChange(sender: UISegmentControl)
{
// Few lines
// self.tableMenu.reloadData()
}
Screenshot 1 :
Screenshot 2 :
So ideally, in screenshot 2, the cart view should have been collapsed.
Update :
Show/Hide view :
func showHideCartView(sender: UIButton)
{
let cell = self.tableMenu.cellForRow(at: IndexPath(row: 0, section: Int(sender.accessibilityHint!)!)) as! RestaurantMenuItemCell
if sender.tag == 1
{
// Show cart view
cell.buttonArrow.tag = 2
cell.viewAddToCart.isHidden = false
cell.constraint_Height_viewAddToCart.constant = 50
cell.buttonArrow.setImage(UIImage(named: "arrowUp.png"), for: .normal)
}
else
{
// Show cart view
cell.buttonArrow.tag = 1
cell.viewAddToCart.isHidden = true
cell.constraint_Height_viewAddToCart.constant = 0
cell.buttonArrow.setImage(UIImage(named: "arrowDown.png"), for: .normal)
}
self.tableMenu.reloadData()
}
cellForRowAtIndexPath :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantMenuItemCell", for: indexPath) as! RestaurantMenuItemCell
cell.selectionStyle = .none
let menuItem = self.menuItems[indexPath.section]
cell.imageViewMenuItem.image = UIImage(named: "recommend0#2x.png")
cell.labelName.text = menuItem.name
cell.labelDescription.text = menuItem.description
cell.labelPrice.text = String(format: "$%i", menuItem.price!)
cell.buttonArrow.accessibilityHint = String(format: "%i", indexPath.section)
cell.buttonArrow.addTarget(self, action: #selector(self.showHideCartView(sender:)), for: .touchUpInside)
return cell
}

Since you rely on the sender button's tag to determine whether the cell should be shown in expanded or collapsed state, you need to make sure that when the segment changes, the tags for all the cells' buttonArrow also change to 1.
Unless that happens, the cells will be reused and since the buttonArrow's tag is set to 2, it will be shown as expanded.

You are reusing cells, see first line of your cellForRowAt implementation:
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantMenuItemCell", for: indexPath) as! RestaurantMenuItemCell
That means that a tableView does not create a new cell, nor it redraws it unless necessary for the given indexPath. However, in the code you expand a cell by setting some height constraints and isHidden flags on subviews of the cell. Now once you reload table at a new segment, the tableView will use the already rendered cells to make the reload as efficient as possible. However, that means, that although you will change the data in the cell, unless you explicitly collapse it, it will stay expanded (because previously you have set the constraints to expanded height).
You need to reset the expanded state when you change the segment. Now let's elaborate, because I believe your current solution has a hidden bug in it:
First of all, since as I said you are dequeueing the cells, in case there are a lot of items in the table and you are scrolling through them, there is a high chance that the expanded cell will get reused even somewhere later in the table view. So it will seem like there is some random item expanded too.
Therefore I suggest you to provide some model that would remember which cells are supposed to be expanded and then use the information in this model to update state of each cell before you return them in your cellForRowAt. The model can be for example a simple array of integer values that would represent indexes of cells that are expanded - so for example if there is an index 3 in the array, that would mean that cell at row 3 should be expanded. Then when you dequeue a cell in cellForRowAt for indexPath with row = 3 you should set constraints on that cell before returning it. This way you can remove the bug I mentioned. Moreover, then when you change segments, you can just remove all the indexes from the array to signify that no cell should be expanded anymore (do it before calling tableView.reloadData()).

What happened here, When you click any row then It will be expanded based on make true first condition in showHideCartView method and after that when you are changing segment index that keep height of previous row as it is because you are reusing cell So you must set normal hight of each row when you change segment index.
For do that take object of indexPath like
var selectedIndexPath: NSIndexPath?
Then set selectedIndexPath in showHideCartView method
func showHideCartView(sender: UIButton) {
selectedIndexPath = NSIndexPath(row: 0, section: Int(sender.accessibilityHint!)!)) // you can change row and section as per your requirement.
//// Your other code
}
Then apply below logic whenever you are changing segment index.
#IBAction func segmentOnChange(sender: UISegmentControl)
{
if indexPath = selectedIndexPath { // check selectedIndexPath is not nil
let cell = self.tableMenu.cellForRow(at: indexPath) as! RestaurantMenuItemCell
cell.buttonArrow.tag = 1
cell.viewAddToCart.isHidden = true
cell.constraint_Height_viewAddToCart.constant = 0
cell.buttonArrow.setImage(UIImage(named: "arrowDown.png"), for: .normal)
}
selectedIndexPath = nil // assign nil to selectedIndexPath
// Few lines
self.tableMenu.reloadData() // Reload your tableview
}

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()
}

change the objects of just one collectionview cell

What I'm trying to do is when I tap the button in a cell, that button in that cell becomes invisible. The problem is when I tap the button, it becomes invisible, but when I scroll the collection view the hidden button goes from one to the other. For example, I tap the second one it hides but when I scroll I see that the 7th becomes hidden. Every time I scroll the hidden button change.
This is the code I wrote:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell : CollectionViewCellKharid3 = collectionView.dequeueReusableCell(withReuseIdentifier: "customcell3", for: indexPath) as! CollectionViewCellKharid3
cell.lblEsmeMahsul.text = mainCats[indexPath.row]
cell.imgMahsul.af_setImage(withURL: URL(string : (mainadress + "/Opitures/" + mainPicNumbers[indexPath.row]))!, placeholderImage: UIImage(named: "loadings" ))
cell.btnKharid.addTarget(self, action: #selector(btnColectionviewCellTapped), for : UIControlEvents.touchUpInside)
cell.btnKharid.tag = indexPath.row
cell.btnMosbat.addTarget(self, action: #selector(btnMosbatTapped), for : UIControlEvents.touchUpInside)
cell.btnMosbat.tag = indexPath.row
cell.configureCell()
return cell
}
#objc func btnColectionviewCellTapped(_ sender:UIButton){
// let indexPath : IndexPath = self.collectionview1.ind
print(sender.tag)
sender.isHidden = true
}
#objc func btnMosbatTapped(_ sender:UIButton){
let index = IndexPath(item: sender.tag , section: 0)
let cell = self.collectionviewForushVije.cellForItem(at: index) as? CollectionViewCellKharid3
cell?.lblTedad.text = "22"
print(sender.tag)
}
Cells get reused. You need to keep track of which cells have been tapped so you can set the proper button state in your cellForItemAt method.
Declare a property in your class:
var beenTapped: Set<Int> = []
Then in btnColectionviewCellTapped add:
beenTapped.insert(sender.tag)
And in cellForItemAt you need:
cell.btnKharid.isHidden = beenTapped.contains(indexPath.item)
You should also replace the use of indexPath.row with indexPath.item. row is for table views. item is for collection views.
It's a very common mis-use of UICollectionView(or UITableView). When deal with them, you should alway keep one thing in mind, re-use. The collection/tableview cell will be highly reuse by os when on need. The problem cause in your code is, you assume the one time set of one property in a cell will be persistence, which is wrong. The cell come from dequeue method, can always be a new cell or an existing cell, therefore, any configuration should be apply to a cell should be config again. Think in that way, all view in a cell is "dirty" when it get it from collection view, you should set the property you want before return it back(or have a mechanism to set it later). Therefore, in your case, just set the isHidden property every time you prepare the cell in cellForRow delegate.

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.

Two TableViews One Data Source?

I have setup a segue that will show a view controller with a small TableView. I want a different segue to show a bigger TableView but I want the bigger table to have the same exact info as the smaller table. Got the smaller tableView working perfect on its own, but once I give the bigger table a Data source, reset and try it out.....crashes.
// IndexPath or First Cell in TableView
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = UITableViewCell()
if self.TaskTableViews.hidden == false {
cell = tableView.dequeueReusableCellWithIdentifier( "FirstTask" , forIndexPath: indexPath) as UITableViewCell!
let list = frc.objectAtIndexPath(indexPath) as! List
cell.textLabel?.text = list.taskName
cell.textLabel?.textColor = UIColor.whiteColor()
TaskTableViews.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.55)
TaskTableViews.layer.cornerRadius = 8
TaskTableViews.separatorColor?.colorWithAlphaComponent(2.0) }
if self.TaskTable2.hidden == false {
cell = tableView.dequeueReusableCellWithIdentifier( "Second Task" , forIndexPath: indexPath) as UITableViewCell!
let list = frc.objectAtIndexPath(indexPath) as! List
cell.textLabel?.text = list.taskName
cell.textLabel?.textColor = UIColor.whiteColor()
TaskTable2.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.55)
TaskTable2.layer.cornerRadius = 8
TaskTable2.separatorColor?.colorWithAlphaComponent(2.0) }
return cell as UITableViewCell
}
The problem is that the code for your two tables is slamming into each other. To fix this, rejigger your logic. Do not make your logic depend on what is hidden. You can handle only one table at a time; just one table is calling here. That table comes in as the tableView parameter. Make your logic depend on that. Depending what table view that tableView parameter is, configure the cell and return it for that table view.
I think that you should consider changing your approach by just resizing the table view dynamically when you go from one scene to the other instead of having two table views if the information is exactly the same.
If you're still pushing for this approach then don't make the condition be that the table view is hidden and instead implement your own logic or boolean to determine this. But again, I'd rather resize a single table view as needed.
You have many problems in your code
1.
var cell = UITableViewCell()
What's the point of this line?
2.
cell = tableView.dequeueReusableCellWithIdentifier( "FirstTask" , forIndexPath: indexPath) as UITableViewCell!
What's the point of casting? This function returns UITableViewCell (not even optional)
3.
cell.textLabel?.text = list.taskName
Should not compile, cause UITableViewCell doesn't have textLabel
4.
TaskTableViews.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.55)
What's the point of doing this every cell request? Move this to viewDidLoad or other appropriate place
Your if { } parts are identical except of reuse identifier
Use tableView argument when needed
Use if { } else
8.
return cell as UITableViewCell
Why cast? Just return
I'm sure I haven't found all of them))

How to hide content on specific cells only, despite using dequeueReusableCell?

I would like to hide some elements in a custom cell when we overpass a specific number of row. I added more row than the ones visible, because I needed to scroll until the last row without the bouncing effect. But now I have more cells, and I don't need the cells after row > 13.
I tried to setNeedsDisplay the cell with a if else, but the dequeue... method has a bad effect on the cells, when I scroll up, back to the previous cells, they don't have the texts anymore, like the row > 13. Is there a way to use the dequeue method, and let the content for the rows < 13, and remove the content for the rows > 13 ?
Here is some code :
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var identifier = ""
if tableView == self.tableView{
identifier = "MyCell"
let cell = tableView.dequeueReusableCellWithIdentifier(identifier) as MyCell
if indexPath.row < 14 {
cell.showContent = true
cell.label.text = "test\(indexPath.row)"
}
else {
cell.showContent = false
cell.label.text = ""
cell.addItem.text = ""
}
cell.setNeedsDisplay()
return cell
}
//MyCell
override func drawRect(rect: CGRect) {
if !showContent {
label.text = ""
addItem.text = ""
}
else {
let path = UIBezierPath()//custom separator that should not be drawn for row > 13
Thanks
You shouldn't modify the text this way in drawRect. You already modified the labels in cellForRow. That's all you need.
That said, this isn't how I would do it. I'd probably create a different cell with its own identifier for empty cells. That way they can be really simple and you don't have to do things like cell.setNeedsDisplay() to get rid of the separator line. So in cellForRow, just return one kind of cell for data rows, and a different kind of cell for empty rows. There's no rule that says all the cells have to be the same class.

Resources