How to configure stackview with dynamic content inside UITableViewCell? - ios

I have UITableViewCell which looks like CardView. Inside cell, I should display some dynamic content. That is why I have stackview inside my cell that will have my dynamic content. But the problem is that cellForRowAt method is called every time while scrolling and stackview starts having extra elements.
How should I solve this problem?
How I handled problem by myself?
I found prepareForReuse method of UITableViewCell which is called before re-configuring by cell. In that method, I clean my stackView. And now, my stackview will not have extra views. But, it is too bad for performance. The next way I tried is holding some flag inside my cell that tells me was stackview already configured. But, this approach didn't help (yes, it will not add extra elements, but content inside cells gets incorrect placement) as checking stackview length.
Here is my pseudo-code:
/// called in cellForRowAt
func configure(item: Item) {
item.forEach {
stackView.addArrangedSubview(ItemView(item))
}
}
func prepareForReuse() {
stackView.arrangedSubviews.forEach {
$0.removeFromSuperView()
}
}

If the prepareForReuse and dequeuing methods leads to exceeding 0.0167 sec (60 frames per second) then maybe in your edge case it will be better to create a cell instead of dequeuing it.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.configure(item: item)
return cell
}

Related

Some custom UITableViewCells are not rendering

I have a custom cell UITableViewCell, sometimes when the tableview loads, I am getting missing cells (cells where the content view is not rendering at all. I get this behavior fairly consistently when I use reloadRowsAtIndexPath (when a custom object the cell is using is updated for example). If I call reloadData on the tableview, I usually don't get this behavior.
Here is what it looks like when view debugging:
Here is the cell under that (which rendered fine):
My initialization of the cell in cellForRowAtIndexPath is the usual pattern:
Edit - entire cellForRowAtIndexPath:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let reuseIdentifier = "WorkOrderListCell"
let cell:WorkOrderListCell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) as! WorkOrderListCell
If I scroll the tableview so that the cell which did not render is off screen, and then scroll back, the cell will render.
I have also ensured that I am on the main queue by wrapping my reloadRowsAtIndexPath in a main queue closure but that doesn't make a difference.
What am I missing?
Some times there's an issue with table view on first load. So I would suggest reloading tableView data twice. You can use this extension:
extension UITableView {
func reloadDataWithAutoSizingCellWorkAround() {
self.reloadData()
self.setNeedsLayout()
self.layoutIfNeeded()
self.reloadData()
}
}
Which is found in this issue https://github.com/smileyborg/TableViewCellWithAutoLayoutiOS8/issues/10
Or you can call directly:
self.reloadData()
self.setNeedsLayout()
self.layoutIfNeeded()
self.reloadData()
Issue was that I had a ambiguous constraint in the content view of the cell. It only got reported when I added setNeedsLayout in the delegate method to reload the row. Once I found that, it was easy to find using the a breakpoint and debugging the view. When I removed the distance constraint that was causing the issue, all the rendering issues went away. Thanks to #DionizB for putting me on a good path.

Reusing dynamic TableViewCells without having to maintain of each cell's state in Controller

I have a UITableView that uses a cell that has 3 expandable and collapsable subviews in them. I would prefer to maintain the state of these views in my UITableViewCell class itself (states as in collapsed or expanded)
Since they are reusable cells, currently, if I expand view 1 in cell A, and then scroll down to cell B, it's view 1 will be expanded. I don't want this. I want it collapsed. But, if I scroll back up to cell A, I want it to still be expanded.
Other than storing all of these states in an array or dictionary
var expandedViewOneCells: [Int] = []
var expandedViewTwoCells: [Int] = []
etc.
I would prefer to have the cells essentially of act individually and maintain their own state... But how would I do this when cells are reused? Keep in mind, I will always only have at most 3 of these kinds of cells, so can I set something like only reuse after 3 cells.
Would it be wise to keep an array of the cells I load, and then on cellForRowAt load the cell from that array based on the index and return it?
In your func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell function try not to deque a cell but create a new instance of your cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = MyCustomCell()
return cell
}
If you are loading your cell from a xib file you need a way to create your custom cell from that nib. Add the following method to your CustomCell class
static func loadFromNib() -> RequestTableViewCell {
let nib = UINib(nibName: "\(MyCustomCell.self)", bundle: Bundle.main)
let cell = nib.instantiate(withOwner: self, options: nil)[0] as! MyCustomCell
return cell
}
Then in your func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell this will create a new cell for every row and not reuse a cell when scrolling
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = MyCustomCell.loadFromNib()
return cell
}
A solution like this may not be optimal if your table view has a lot of rows but for a SMALL amount of rows this should be okay
I see 2 solutions to your problem:
Use 3 View Controllers. They should never get destroyed, and add / remove the corresponding VC's view on top of the .contentView of the cell as it appears or goes off-screen. See the solution here http://khanlou.com/2015/04/view-controllers-in-cells/ The Custom Cell itself is just a view, shouldn't really be concerned with the state, but if we move that logic to a View Controller - we should be fine, an we are not violating MVC. Plus, the View Controller can keep track of the height of the view, based on the state, and heightForRow(at:) can ask it for that
I'd use a Stack View as this is a perfect scenario for it. I'd probably represent the Cell itself as another stack view. Not sure exactly what the views look like and how they change, but it may end up as simple as hiding / unhiding the second view from the Stack View that represent a "cell".

UIStackView in UITableView cell causes jerking when scrolled

I have a UITableView cell with a stack view inside. When the cell is tapped the data source changes and the table view is reloaded. The stack view will now have more views inside and the cell is bigger. However sometimes when I scroll the table there is jerky behaviour. It's almost like the cell size was calculated wrong or something (even though it looks fine). Once the tableview has jerked once it is fine and doesn't do it again until I tap the cell and it adds more stack views.
I am using UITableViewAutomaticDimension on the table view. I have tried removing the cell and the table doesn't jerk. It's defiantly the stack view causing issues.
I set my estimated row height to as close as possible to the calculated height tableView.estimatedRowHeight = 270. No affect. I have also tried implementing the delegate and it makes no difference. I have tried many combinations or sizes and the result is the same. Any idea on what I am doing wrong here? Do stack views in cells just suck?
I think you are on the right track about estimatedRowHeight causing trouble. I encounter this jerking problem and in pretty much every tableView with varying element size. What usually does the job is "caching" cell heights and returning them in delegate, something like:
class MyViewController: UIViewController {
fileprivate var cachedCellHeights = [IndexPath: CGFloat]()
//your code here
}
extension MyViewController: UITableViewDelegate {
public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cachedCellHeights[indexPath] = cell.frame.height
}
public func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = cachedCellHeights[indexPath] {
return height
}
return 270
}
}
It should work as long as you configure your cell (i.e. add new views to stack view) in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath).
The same applies to section headers.

How to create tableview cells without using prototype cells?

In my tableview, every cell will be different and determined by a JSON response from server. And there will be infinite possibilities. So defining a prototype for each type of cell is not possible.
For example, one cell will have labels and buttons, another cell have images and buttons in different orders.
How to achieve this dynamic structure in tableview cells?
Currently what I am doing is: adding views as subview in cellForRowAtIndexPath but scrolling is very laggy this way.
How to achieve this without affecting performance this much
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! MyCell
for myview in data[indexPath.row].myviews{
cell.addSubview(myview)
}
return cell
}
If you're using a table view then your content is going to scroll vertically, right?
There is a physical limit to the amount of UI that you can put horizontally. Limited by the screen size.
So I'm guessing your UI parts are being laid out vertically in the cell?
So instead of laying out a button, label, image, another button, and a text field vertically in a cell...
Create a cell type called ButtonCell, LabelCell, ImageCell, MultiLineLabelCell, TextFieldCell, etc...
So now, instead of creating one cell with all these elements added. You instead create multiple cells each containing one type of UI. Now you can dequeue your cells in any particular order (driven by your JSON) and won't lose the performance.
The only solution I see is to have empty cell and add/remove subviews as needed. But you should add new subviews to a cell only if you did not add them before.
For example:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as! MyCell
if cell.contentView.viewWithTag(1) == nil {
let label = UILabel()
label.tag = 1
cell.contentView.addSubview(label)
}
let label = cell.contentView.viewWithTag(1)
// label config there
return cell
}
Also don't forget to add subviews to cell's contentView not to cell itself.

UITableViewCell layout not updating until cell is reused

I have a UITableView that I fill with autosizing cells.
UITableView setup is fairly simple:
tableView.estimatedRowHeight = 70
tableView.rowHeight = UITableViewAutomaticDimension
Exactly like Apple recommends here: https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSelf-SizingTableViewCells.html
To enable self-sizing table view cells, you must set the table view’s
rowHeight property to UITableViewAutomaticDimension. You must also
assign a value to the estimatedRowHeight property. As soon as both of
these properties are set, the system uses Auto Layout to calculate the
row’s actual height.
When configuring a cell I also disable/enable some constraints to achieve the needed look. That’s where things get interesting. Cell layout is not updated until the cell is reused. Literally. You can call layoutIfNeeded(), setNeedsLayout(), layoutSubviews() or any other method there is, there is no way you will force the cell to update its layout.
All other aspects work pretty good: labels do change their text, you hide/unhide the views, but layout is stuck until the cell is reused.
Question: what causes it and how to avoid this behavior?
I had your problem too. Instead of remove
tableView.estimatedRowHeight = 70
I just added a layoutIfNeeded at the end of the cellForRow method, just before return the cell itself:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as? MyCustomCell
...
cell?.layoutIfNeeded()
return cell!
}
Result: the cell layout is perfect always, the first time and every after reuse.
Unfortunately, none of the provided answers/comments worked out for me. I always ended up with an initially incorrect layout. Only after reusing the cell, or calling reloadData() (on the table view) it was displayed correctly.
The following was the only thing, that worked for me in the end. I'm not a big fan of such hacks, but after spending about half a day on this seemingly very simple layout issue, I just gave up and went with it. >.<
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Alternatively you could also call reloadData it in viewDidAppear (without the DispatchQueue hack), but then you can clearly see the "jump" when the layout jumps from "incorrect" to "correct".
Anyway, just wanted to share my experience and hope this helps someone else. Cheers!
In my case, the issue was caused by estimatedRowHeight.
Simply removing this line
tableView.estimatedRowHeight = 70
fixed my problems. Cell properly updated its layout and it almost fixed my issues.
But, most likely, you’re going to get another trouble, with your cell’s height being set to 43.5 points. You log will also be filled with auto layout errors, that will include a line like this
<NSLayoutConstraint:0x600000097570 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7fd4ee511d20.height == 43.5 (active)>
Apparently, if you won’t provide estimatedRowHeight, table view puts a 43.5 points height constraint on your cell’s content view, and if your cell’s “internal” height will not match (and probability of that is 99.99%), then it’s going to put errors in log.
How to avoid that error? I don’t know yet.
I post a question about that, and as soon as I find an answer, I will provide a link in this question.
Cell layout is not updated until cell is reused
If you want tableview to reflect changed cell layout.
After changing the cell Layout redraw the table view
tableView.beginUpdates()
tableView.setNeedsDisplay()
tableView.endUpdates()
For instance:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CustomCell else { return }
cell.collapseDescriptionLabel()
// redraw the tableView
tableView.beginUpdates()
tableView.setNeedsDisplay()
tableView.endUpdates()
}
You don't need to use layoutIfNeeded(), setNeedsLayout(), and layoutSubviews() to force the layout. You can use tableView.beginUpdates() and tableView.endUpdates().
For instance:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
tableView.beginUpdates()
cell.heightConstraint.constant = 50
tableView.endUpdates()
}

Resources