Some custom UITableViewCells are not rendering - ios

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.

Related

How to configure stackview with dynamic content inside UITableViewCell?

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
}

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

How can I clear a WKWebview that is contained in a custom UITableViewCell that is reused?

I have a UITableView that dequeues my custom UITableViewCell. The UITableViewCell contains a WKWebView. In cellForRowAtIndexPath, I set the url that the WKWebView should load and the webview starts to load the page. The problem is that the data of the cell, sometimes displays the "old" data while the new url is still loaded.
I tried to first load about:blank and I also tried webView.loadHTMLString("", baseURL: nil), but it did not work, because it is an async operation.
Here is pieces of my code.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: MyTableViewController.cellIdentifier, for: indexPath) as! MyTableViewCell
cell.url = data[(indexPath as NSIndexPath).row]
return cell
}
In MyTableViewCell, I immediately start to load the html. loadHTML(in:,forUrl:) is a private function that loads the url into the webview.
var url: String? {
didSet {
loadHTML(in:webView!, forURL:url)
}
}
It will be much appreciated if someone knows how to do this properly. Some suggestions that I have seen included, hiding the webView until it is finished loaded. I would actually like my user to see the progress in loading.
I don't think it would be a good idea not to dequeue the UITableViewCell. (The cell contain other controls too.) The best I can think of right now, is to remove the WKWebView and adding it again.
What about covering the content with an empty view as long as it is not reloaded (i guess you can check it with the wk delegate didFinishNavigation)
You may leave out the loading, bar tuning a bit the constraint.
It's very hacky i know, but may work if you look for a quick'n'dirty solution.
Not sure as performances how better it is, but for sure it doesn't involve recomputing the constraint as opposite to removing and re-adding the webview

Why using SDWebImage in a UITableViewCell triggers ImageViews not to render properly?

I have a UITableViewController containing my own cells that I dequeue in cellForRowAtIndexPath.
After dequeuing, I configure the cell and reload, asynchronously, the image for that cell.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCellWithIdentifier("PeopleCell", forIndexPath: indexPath) as? PeopleListViewCell {
cell.configureCell(headImageUrl)
}
}
In my PeopleListView class,
func configureCell(img:NSURL) {
if headImageView == nil { // to avoid allocation memory if not used
headImageView = UIImageView()
addSubView(headImageView)
}
headImageView.sd_cancelCurrentImageLoad()
headImageView.sd_setImageWithURL(headImageUrl)
}
It works fine at the first loading and also while scrolling.
But when I push another viewController after
didSelectRowAtIndexPath
and come back to the list after the
dismissViewController()
I end up with a weird effect on my UIImageView , it's kind of a stacked or ghost image effect..
I'm having a hard time to figure out where is even triggered as when I m coming back from the viewController, cellForRowAtIndexPath is not called.
This actually has nothing to do with asynchronous image loading.
The images I had was displayed within circles, with a cornerRadius.
Somehow, it was displayed without any problem at first load...
The issue here is I simply forgot the
headImageView.layer.maskToBounds = true
The result I got before setting maskToBounds to true was that I had the feeling that multiple images were located within the headImageView (UIImageView). If you ever have some artefacts like that, I hope this question/answer will help you.

How to update self-sizing tableview's height?

If I put a custom view in a cell , how can I notify tableview to update cell's height if my custom view's height has changed? I tried invalidateIntrinsicContentSize but that doesn't work. ReloadData works but I doubt whether using
self.superview?.superview?.superview as! UITableView).reloadData()
is a good implementation.
I have seen a similar question here but that's all about view from standard library. And this and this have no answer.
You are thinking in the right direction. However there are two problems with your approach:
reloadData reloads the whole table view and it does not animate the change.
Moving up the superview chain is bound to break when Apple changes the UITableView view hierarchy. They have done that before, so they might do it again.
To fix the first issue you should call reloadRowsAtIndexPaths:withRowAnimation: instead. That only reloads the cells that you specify in the indexPath array. So you pass it an array that only contains the indexPath of your cell.
The second issue is a bit trickier because a UITableViewCell has no reference to its UITableView (and it shouldn't). So it cannot tell the UITableView directly to reload the cell.
You can give each cell a closure that it should execute whenever its height has changed. So you just add a closure property in your custom cell:
class YourCustomTableViewCell: UITableViewCell {
var resizeClosure: (() -> Void)?
...
And you set this closure in your UITableViewControllerDataSource when you dequeue the cell:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("YourCustomCellIdentifier", forIndexPath: indexPath)
if let customCell = cell as? YourCustomTableViewCell {
customCell.resizeClosure = { [weak cell, weak tableView] in
if let currentIndexPath = tableView?.indexPathForCell(cell!) {
tableView?.reloadRowsAtIndexPaths([currentIndexPath], withRowAnimation: .Automatic)
}
}
}
return cell
}
Just make sure that you add tableView to the closure's capture list to avoid a strong reference cycle. That is done by adding the [weak tableView] to the closure.
And then when the cell changes its height you just execute the closure and the cell will be reloaded:
func someFunctionThatChangesTheHeight() {
// change the height
resizeClosure?()
}

Resources