UITableView dynamic cell heights only correct after some scrolling - ios

I have a UITableView with a custom UITableViewCell defined in a storyboard using auto layout. The cell has several multiline UILabels.
The UITableView appears to properly calculate cell heights, but for the first few cells that height isn't properly divided between the labels.
After scrolling a bit, everything works as expected (even the cells that were initially incorrect).
- (void)viewDidLoad {
[super viewDidLoad]
// ...
self.tableView.rowHeight = UITableViewAutomaticDimension;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"TestCell"];
// ...
// Set label.text for variable length string.
return cell;
}
Is there anything that I might be missing, that is causing auto layout not to be able to do its job the first few times?
I've created a sample project which demonstrates this behaviour.

I don't know this is clearly documented or not, but adding [cell layoutIfNeeded] before returning cell solves your problem.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"TestCell"];
NSUInteger n1 = firstLabelWordCount[indexPath.row];
NSUInteger n2 = secondLabelWordCount[indexPath.row];
[cell setNumberOfWordsForFirstLabel:n1 secondLabel:n2];
[cell layoutIfNeeded]; // <- added
return cell;
}

This worked for me when other similar solutions did not:
override func didMoveToSuperview() {
super.didMoveToSuperview()
layoutIfNeeded()
}
This seems like an actual bug since I am very familiar with AutoLayout and how to use UITableViewAutomaticDimension, however I still occasionally come across this issue. I'm glad I finally found something that works as a workaround.

Adding [cell layoutIfNeeded] in cellForRowAtIndexPath does not work for cells that are initially scrolled out-of-view.
Nor does prefacing it with [cell setNeedsLayout].
You still have to scroll certain cells out and back into view for them to resize correctly.
This is pretty frustrating since most devs have Dynamic Type, AutoLayout and Self-Sizing Cells working properly — except for this annoying case. This bug impacts all of my "taller" table view controllers.

I had same experience in one of my projects.
Why it happens?
Cell designed in Storyboard with some width for some device. For example 400px. For example your label have same width. When it loads from storyboard it have width 400px.
Here is a problem:
tableView:heightForRowAtIndexPath: called before cell layout it's subviews.
So it calculated height for label and cell with width 400px. But you run on device with screen, for example, 320px. And this automatically calculated height is incorrect. Just because cell's layoutSubviews happens only after tableView:heightForRowAtIndexPath:
Even if you set preferredMaxLayoutWidth for your label manually in layoutSubviews it not helps.
My solution:
1) Subclass UITableView and override dequeueReusableCellWithIdentifier:forIndexPath:. Set cell width equal to table width and force cell's layout.
- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [super dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
CGRect cellFrame = cell.frame;
cellFrame.size.width = self.frame.size.width;
cell.frame = cellFrame;
[cell layoutIfNeeded];
return cell;
}
2) Subclass UITableViewCell.
Set preferredMaxLayoutWidth manually for your labels in layoutSubviews. Also you need manually layout contentView, because it doesn't layout automatically after cell frame change (I don't know why, but it is)
- (void)layoutSubviews {
[super layoutSubviews];
[self.contentView layoutIfNeeded];
self.yourLongTextLabel.preferredMaxLayoutWidth = self.yourLongTextLabel.width;
}

none of the above solutions worked for me, what worked is this recipe of a magic:
call them in this order:
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.beginUpdates()
tableView.endUpdates()
my tableView data are populated from a web service, in the call back of the connection I write the above lines.

I have a similar problem, at the first load, the row height was not calculated but after some scrolling or go to another screen and i come back to this screen rows are calculated. At the first load my items are loaded from the internet and at the second load my items are loaded first from Core Data and reloaded from internet and i noticed that rows height are calculated at the reload from internet. So i noticed that when tableView.reloadData() is called during segue animation (same problem with push and present segue), row height was not calculated. So i hidden the tableview at the view initialization and put an activity loader to prevent an ugly effect to the user and i call tableView.reloadData after 300ms and now the problem is solved. I think it's a UIKit bug but this workaround make the trick.
I put theses lines (Swift 3.0) in my item load completion handler
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300), execute: {
self.tableView.isHidden = false
self.loader.stopAnimating()
self.tableView.reloadData()
})
This explain why for some people, put a reloadData in layoutSubviews solve the issue

I have tried most of the answers to this question and could not get any of them to work. The only functional solution I found was to add the following to my UITableViewController subclass:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.endUpdates()
}
}
The UIView.performWithoutAnimation call is required, otherwise you will see the normal table view animation as the view controller loads.

In my case the last line of the UILabel was truncated when the cell was displayed for the first time. It happened pretty randomly and the only way to size it correctly was to scroll the cell out of the view and to bring it back.
I tried all the possible solutions displayed so far (layoutIfNeeded..reloadData) but nothing worked for me. The trick was to set "Autoshrink" to Minimuum Font Scale (0.5 for me). Give it a try

Add a constraint for all content within a table view custom cell, then estimate table view row height and set row hight to automatic dimension with in a viewdid load :
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 70
tableView.rowHeight = UITableViewAutomaticDimension
}
To fix that initial loading issue apply layoutIfNeeded method with in a custom table view cell :
class CustomTableViewCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
self.layoutIfNeeded()
// Initialization code
}
}

None of the above solutions worked but the following combination of the suggestions did.
Had to add the following in viewDidLoad().
DispatchQueue.main.async {
self.tableView.reloadData()
self.tableView.setNeedsLayout()
self.tableView.layoutIfNeeded()
self.tableView.reloadData()
}
The above combination of reloadData, setNeedsLayout and layoutIfNeeded worked but not any other. Could be specific to the cells in the project though. And yes, had to invoke reloadData twice to make it work.
Also set the following in viewDidLoad
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = MyEstimatedHeight
In tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
cell.setNeedsLayout()
cell.layoutIfNeeded()

calling cell.layoutIfNeeded() inside cellForRowAt worked for me on ios 10 and ios 11, but not on ios 9.
to get this work on ios 9 also, I call cell.layoutSubviews() and it did the trick.

Setting preferredMaxLayoutWidth helps in my case.
I added
cell.detailLabel.preferredMaxLayoutWidth = cell.frame.width
in my code.
Also refer to Single line text takes two lines in UILabel and http://openradar.appspot.com/17799811.

In my case, a stack view in the cell was causing the problem. It's a bug apparently. Once I removed it, the problem was solved.

For me none of these approaches worked, but I discovered that the label had an explicit Preferred Width set in Interface Builder. Removing that (unchecking "Explicit") and then using UITableViewAutomaticDimension worked as expected.

I tried all of the solutions in this page but unchecking use size classes then checking it again solved my problem.
Edit: Unchecking size classes causes a lot of problems on storyboard so I tried another solution. I populated my table view in my view controller's viewDidLoad and viewWillAppear methods. This solved my problem.

For iOS 12+ only, 2019 onwards...
An ongoing example of Apple's occasional bizarre incompetence, where problems go on for literally years.
It does seem to be the case that
cell.layoutIfNeeded()
return cell
will fix it. (You're losing some performance of course.)
Such is life with Apple.

I have the issue with resizing label so I nee just to do
chatTextLabel.text = chatMessage.message
chatTextLabel?.updateConstraints() after setting up the text
// full code
func setContent() {
chatTextLabel.text = chatMessage.message
chatTextLabel?.updateConstraints()
let labelTextWidth = (chatTextLabel?.intrinsicContentSize().width) ?? 0
let labelTextHeight = chatTextLabel?.intrinsicContentSize().height
guard labelTextWidth < originWidth && labelTextHeight <= singleLineRowheight else {
trailingConstraint?.constant = trailingConstant
return
}
trailingConstraint?.constant = trailingConstant + (originWidth - labelTextWidth)
}

In my case, I was updating in other cycle. So tableViewCell height was updated after labelText was set. I deleted async block.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:Identifier, for: indexPath)
// Check your cycle if update cycle is same or not
// DispatchQueue.main.async {
cell.label.text = nil
// }
}

Just make sure you're not setting the label text in 'willdisplaycell' delegate method of table view.
Set the label text in 'cellForRowAtindexPath' delegate method for dynamic height calculation.
You're Welcome :)

The problem is that the initial cells load before we have a valid row height.
The workaround is to force a table reload when the view appears.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.tableView reloadData];
}

In my case, the issue with the cell height takes place after the initial table view is loaded, and a user action takes place (tapping on a button in a cell that has an effect of changing the cell height). I have been unable to get the cell to change its height unless I do:
[self.tableView reloadData];
I did try
[cell layoutIfNeeded];
but that didn't work.

In Swift 3. I had to call self.layoutIfNeeded() each time I update the text of the reusable cell.
import UIKit
import SnapKit
class CommentTableViewCell: UITableViewCell {
static let reuseIdentifier = "CommentTableViewCell"
var comment: Comment! {
didSet {
textLbl.attributedText = comment.attributedTextToDisplay()
self.layoutIfNeeded() //This is a fix to make propper automatic dimentions (height).
}
}
internal var textLbl = UILabel()
override func layoutSubviews() {
super.layoutSubviews()
if textLbl.superview == nil {
textLbl.numberOfLines = 0
textLbl.lineBreakMode = .byWordWrapping
self.contentView.addSubview(textLbl)
textLbl.snp.makeConstraints({ (make) in
make.left.equalTo(contentView.snp.left).inset(10)
make.right.equalTo(contentView.snp.right).inset(10)
make.top.equalTo(contentView.snp.top).inset(10)
make.bottom.equalTo(contentView.snp.bottom).inset(10)
})
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let comment = comments[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: CommentTableViewCell.reuseIdentifier, for: indexPath) as! CommentTableViewCell
cell.selectionStyle = .none
cell.comment = comment
return cell
}
commentsTableView.rowHeight = UITableViewAutomaticDimension
commentsTableView.estimatedRowHeight = 140

I ran into this issue and fixed it by moving my view/label initialization code FROM tableView(willDisplay cell:) TO tableView(cellForRowAt:).

I found a pretty good workaround for this. Since the heights cannot be calculated before the cell is visible, all you need to do is scroll to the cell before calculating it's size.
tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)
let cell = tableView.cellForRow(at: indexPath) ?? UITableViewCell()
let size = cell.systemLayoutSizeFitting(CGSize(width: frame.size.width, height: UIView.layoutFittingCompressedSize.height))

iOS 11+
table views use estimated heights by default. This means that the contentSize is just as estimated value initially. If you need to use the contentSize, you’ll want to disable estimated heights by setting the 3 estimated height properties to zero: tableView.estimatedRowHeight = 0 tableView.estimatedSectionHeaderHeight = 0 tableView.estimatedSectionFooterHeight = 0
public func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}
public func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return CGFloat.leastNormalMagnitude
}
public func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}

Another solution
#IBOutlet private weak var tableView: UITableView! {
didSet {
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = UITableView.automaticDimension
}
}
extension YourViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
UITableView.automaticDimension
}
}
Then you don't need to layoutSabviews

override the prepareForReuse() and set lable to nil in you cell class
override func prepareForReuse() {
super.prepareForReuse()
self.detailLabel.text = nil
self.titleLabel.text = nil
}

Quick and dirty way. Double reloadData like that:
tableView.reloadData()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { [weak self] in
self?.tableView.reloadData()
})

Just make sure you are not engaging the main thread too much
ie :
Don't use DispatchQueue.main.async in TableViewCell or to set the values... This will engage the main thread and mess up the height of the cell doesn't matter if you use all the above solutions.

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
// call the method dynamiclabelHeightForText
}
use the above method which return the height for the row dynamically.
And assign the same dynamic height to the the lable you are using.
-(int)dynamiclabelHeightForText:(NSString *)text :(int)width :(UIFont *)font
{
CGSize maximumLabelSize = CGSizeMake(width,2500);
CGSize expectedLabelSize = [text sizeWithFont:font
constrainedToSize:maximumLabelSize
lineBreakMode:NSLineBreakByWordWrapping];
return expectedLabelSize.height;
}
This code helps you finding the dynamic height for text displaying in the label.

Related

unable to Set Height of the CustomCells in swift

I have many customCells each cell may have a button which acts like a textbox or textArea.When the View loads they have very less height.
override func viewDidLoad() {
super.viewDidLoad();
self.view.backgroundColor = UIColor.groupTableViewBackground
self.tableView.separatorStyle = UITableViewCellSeparatorStyle.none
self.tableView.estimatedRowHeight = 100
tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.setNeedsLayout()
self.tableView.layoutIfNeeded()
}
And i have the following constraints defined on the cell
Will auto layout work with prototype cells at all?
If not how to implement this?
I tried with HeightforrowAtIndexPath method layout looks good only after scroll. Initial load of the view will not come with good layout.
below is my code for this.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if (eleData.elementType=="apple")
{
return 90;
}
else if (eleData.elementType=="banana")
{
return 50;
}
else
{
return 30;
}
}
Any suggestions .
Thank you in advance.
Quick google search gives you your answer...
UITableView dynamic cell heights only correct after some scrolling
You need to add layout if needed before returning your cell in the cellForRowAtIndexPath method:
cell.layoutIfNeeded()

Expanding UITextView in UITableViewCell similar to App Store "Description" Cell

In the iOS App Store, there is a cell for the Description of the app. If the text is too long, the cell has a blue "more" button which expands the cell to fit the entire text. There is the same functionality for the "What's New" section detailing the information of the latest update. I have tried implementing this with some problems.
Note: I am using AutoLayout in my Storyboard.
I have a UITableViewController subclass and a UITableViewCell subclass.
import UIKit
class SystemDetailDescriptionTableViewCell: UITableViewCell {
static let defaultHeight: CGFloat = 44
#IBOutlet weak var descriptionTextView: UITextView!
}
The descriptionTextView is set in AutoLayout to 0 on the top, bottom, left, and right.
Next, we have a UITableViewController subclass. My first thought was to use the heightForRowAtIndexPath method.
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.section == 0 {
if self.didExpandDescriptionCell {
if let cell = tableView.dequeueReusableCellWithIdentifier(StoryboardPrototypeCellIdentifiers.descriptionCell) as? SystemDetailDescriptionTableViewCell {
return cell.descriptionTextView.contentSize.height
}
}
return SystemDetailDescriptionTableViewCell.defaultHeight
}
return tableView.rowHeight
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.section == 0 {
self.didExpandDescriptionCell = true
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
} else {
if let link = self.links?[indexPath.row] {
self.performSegueWithIdentifier(StoryboardSegueIdentifiers.toVideoView, sender: link)
}
}
}
The problem is that the contentSize is not properly sizing to the full length of the text. Instead, it's about 3/4 of the text. I have heard that this method will not work with AutoLayout and instead some trickery needs to be done with the LayoutManager, but those methods returned the exact same results.
Can anybody give me some insight on why this isn't working as expected?
You need to tell the text viewYou should try saying something like:
let newSize = cell.descriptionTextView.sizeThatFits(CGSize(width: cell.descriptionTextView.bounds.size.width, height: CGFloat.max))
cell.descriptionTextViewHeightConstraint.constant = newSize.height
return newSize.height
You need to get the appropriate height for the content, then update the constraint to that new height (if you've set a height constraint), then update the cell height. This code assumes that there is only descriptionTextView in that cell and that it requires zero padding.

Settings UITableViewCell Heights With Multiline UIButtons in Auto Layout

I have a UITableView with custom UITableViewCells, each has a UIButton inside. I'm setting buttons' titles from an array, so the size of the buttons change according to the title. I need to return correct height based on the inner button's size in heightForRowAtIndexPath event.
Since I'm using auto layout, I've created an outlet for the button's height constraint and I'm updating it in the cell's layoutSubviews() event like this:
class CustomCell: UITableViewCell {
/* ... */
override func layoutSubviews() {
super.layoutSubviews()
self.myButton?.layoutIfNeeded()
self.heightConstraint?.constant = self.myButton!.titleLabel!.frame.size.height
}
}
Then I return the height based on the button height and top-bottom margins like so:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
var cell = tableView.dequeueReusableCellWithIdentifier("CustomCell") as! CustomCell
cell.myButton?.setTitle(self.data[indexPath.row], forState: UIControlState.Normal)
cell.bounds = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds))
cell.setNeedsLayout()
cell.layoutIfNeeded()
return cell.myButton!.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height + (cell.topMarginConstraint!.constant * 2) /* top-bottom margins */ + 1 /* separator height */
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("CustomCell") as! CustomCell
cell.myButton?.setTitle(self.data[indexPath.row], forState: UIControlState.Normal)
return cell
}
On the first launch, there seems to be no problem. However, after I begin scrolling, then the height of some rows seem to be mistaken. When I get back to the top, I see that previous cell heights get to be broken as well.
When I googled for similar problems, issue seems to be about reusable cells, though I was unable to find another way to calculate the height. What can be done to reuse cells correctly or getting the correct height, perhaps by another method?
More info and source code:
Constraints set by IB like this:
Here's the cells on the first launch:
After some scrolling:
Full code of the project can be found on Github.
According to this
Configure tableView as
func configureTableView() {
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 160.0
}
Call it on your viewDidLoad method
Than configure your uibutton height constraint to be greater then or equal.
Override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat where you can place your estimation height code
First off, it's better if you perform constraint updates in func updateConstraints() method of UIView. So instead of
override func layoutSubviews() {
super.layoutSubviews()
self.myButton?.layoutIfNeeded()
self.heightConstraint?.constant = self.myButton!.titleLabel!.frame.size.height
}
I would do
override func updateConstraints() {
self.myButton?.layoutIfNeeded()
self.heightConstraint?.constant = self.myButton!.titleLabel!.frame.size.height
super.updateConstraints()
}
Note that you should call the super implementation at the end, not at the start. Then you would call cell.setNeedsUpdateConstraints() to trigger a constraint update pass.
Also you should never directly manipulate the cell bounds the way you are doing in heightForRowAtIndePath: method, and even if you are completely sure that manipulating directly is what you want, you should manipulate cell.contentView's bounds, not the cell's bounds. If you are looking to adjust the cell height dynamically with respect to the dimensions of the content, you should use self sizing cells. If you need to support iOS 7, then this answer tells you how to achieve that behaviour with autolayout only (without touching the bounds etc).
To reiterate the answer, you should do:
func viewDidLoad() {
self.dummyCell = CustomCell.init()
// additional setup
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
self.dummyCell.myButton?.setTitle(self.data[indexPath.row], forState: UIControlState.Normal)
self.dummyCell.layoutIfNeeded() // or self.dummyCell.setNeedsUpdateConstraints() if and only if the button text is changing in the cell
return self.dummyCell.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
}
Please know that the answer I linked to outlines a strategy to get the cell height via autolayout, so only writing the code changes I proposed won't be enough unless you set your constraints in a way that makes this solution work. Please refer to that answer for more information.
Hope it helps!
First of all, remove the height constraint of button and bind it to top and bottom with cell.
Then, in your cell' height, calculate height of the text based on the width and font of button. This will make the cell's height dynamic and you wont need height constraint anymore.
Refer the link below to get the height of text:
Adjust UILabel height to text
Hope it helps. If you need help further or understanding anything, let me know.. :)

Grouped UITableview remove outer separator line

I have a grouped UITableview which is created programatically. Also I have a cell with xib file populated in tableview programmatically as well. So far so good. But I want to only remove outer separator line. I used below code but this time removed all separator line.
self.tableView.separatorColor = [UIColor clearColor];
this is not good option for my situation. Here is the screenshot what i want to do;
You can remove separators even in grouped UITableView with Static Cell:
class CustomCell: UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
for view in subviews where view != contentView {
view.removeFromSuperview()
}
}
Here's my solution:
self.tableView.separatorColor = self.tableView.backgroundColor
Google, even in 2018, is serving this page as the top result for this question. I didn't have any luck in iOS 11 with any of the provided answers, so here's what I came up with:
extension UITableViewCell {
func removeSectionSeparators() {
for subview in subviews {
if subview != contentView && subview.frame.width == frame.width {
subview.removeFromSuperview()
}
}
}
}
Calling .removeSectionSeparators() on any UITableViewCell instance will now take care of the problem. In my case at least, the section separators are the only ones with the same width as the cell itself (as the other ones are all indented).
The only question left is from where we should call it. You'd think willDisplayCell would be the best choice, but I discovered that the initial call occurs before the separator views themselves are generated, so no dice.
I ended up putting this in my cellForRowAtIndexPath method just before I return a reloaded cell:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyReusableIdentifier", for: indexPath)
Timer.scheduledTimer(withTimeInterval: 0.15, repeats: false) { (timer) in
cell.removeSectionSeparators()
}
return cell
}
It doesn't feel that elegant, but I haven't run into any issues yet.
EDIT: Looks like we need this too (for reused cells):
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.removeSectionSeparators()
}
Here's a before/after in screenshots with this code:
Before
After
I just worked out a solution, as the cell has contentView which is a UIView, so I think you can just focus on the bottomline of contentView.
Here is my code:
first, you have to make the separator to clear
tableView.separatorColor = UIColor.clear
Second, in the cellForRowAt function:
let bottomBorder = CALayer()
bottomBorder.frame = CGRect(x: 0.0, y: 43.0, width: cell.contentView.frame.size.width, height: 1.0)
bottomBorder.backgroundColor = UIColor(white: 0.8, alpha: 1.0).cgColor
cell.contentView.layer.addSublayer(bottomBorder)
here you will see the UI like this:
For removing the top and bottom part of separator line for each section. Add this to your static cell.
override func layoutSubviews() {
super.layoutSubviews()
//Get the width of tableview
let width = subviews[0].frame.width
for view in subviews where view != contentView {
//for top and bottom separator will be same width with the tableview width
//so we check at here and remove accordingly
if view.frame.width == width {
view.removeFromSuperview()
}
}
}
Result as below image
After inspecting the view hierarchy, it seems each UITableViewCell has only three subviews: the content view (UITableViewCellContentView), and two separator views (_UITableViewCellSeparatorView). I'm not a fan of dynamic class instantiation from NSStrings (and neither is Apple 😉). However, because the contentView of a UITableViewCell is accessible without using private APIs, the solution turns out to be pretty easy.
The idea is just to iterate through the subviews of your UITableViewCell, and remove any views that aren't the contentView:
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let subviews = cell.subviews
if subviews.count >= 3 {
for subview in subviews {
if subview != cell.contentView {
subview.removeFromSuperview()
break
}
}
}
}
tableView(:willDisplayCell:forRowAtIndexPath:) is called multiple times during table view rendering, so the above keeps track of state by checking how many subviews the cell has. If it has three subviews, both separators are still intact. It also only removes one of the separators, but you can remove both by removing the break. You can also specify whether to remove the top separator or the bottom separator by checking the subview's frame. If the frame's y-axis origin is 0, that's the top separator. If it's not 0, it's the bottom.
Hope this helps!
Swift 4, Swift 4.2 Update:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let subviews = cell.subviews
guard subviews.count >= 3 else {
return
}
for subview in subviews where NSStringFromClass(subview.classForCoder) == "_UITableViewCellSeparatorView" {
subview.removeFromSuperview()
}
}
iOS 14, Swift 5
In the custom cell class:
override func layoutSubviews(){
super.layoutSubviews()
for subview in subviews where (subview != contentView && abs(subview.frame.width - frame.width) <= 0.1 && subview.frame.height < 2) {
subview.removeFromSuperview() //option #1 -- remove the line completely
//subview.frame = subview.frame.insetBy(dx: 16, dy: 0) //option #2 -- modify the length
}
}
I want to thank #cook for this solution, as I built on top of it. I had some issues with their solution:
it was removing the default highlight/selected background view, so I added an extra check on the subview's height.
I put my code in layoutSubviews() and haven't had a single issue.
I implemented an approximation between two CGFloats instead of using the equality operator ==, which sounds error-prone to me.
I added the "option #2" in the code, as that's the solution I was personally looking for (I wanted to maintain the separator, but I wanted it to be at the same indentation level as the regular cell separators, in my case a value of 16).
That is a really old question, still it's one of the first entries on google when searching for how to remove the top and bottom separators for each section.
After some investigation, I found out that there is no way and just no intention from Apple to make this somehow happen without stupidly complicated hacks of the view hierarchy.
Fortunately there is a absolutely simple and easy way of achieving such a look:
I say simple but for a beginner, this might be difficult because of a lack of understanding how UITableView works and how to implement your own cells. Let me try to explain it:
Create a UITableViewCell subclass
In your cell, create a #IBOutlet weak var separatorView: UIView! property
In your Storyboard, select the tableview cell and select your own cell as the class which backs up the cell. Note: You do not have to use custom style. You still can use Basic, Subtitle, etc.
Set your UITableViews Separator Style to None to hide the default separator
Drag a UIView onto your cell and resize it so it is on the bottom (or the top) of the cell. Use the Size Inspectors Autoresizing to pin it to start/end/bottom and give it a flex width (or Autolayout but thats just over the top in this case)
Connect the view to the outlet of your cell class
In your UITableViewDataSources cellForRowAtIndexPath: set the isHidden property of your custom separator based on if the indexPath.row is the last row (or the first, if your view is at the top) in the section
Heres some example code:
class MyCell: UITableViewCell {
#IBOutlet weak var separatorView: UIView!
}
class ViewController: UITableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MyCell
cell.separatorView.isHidden = indexPath.row == 2
return cell
}
}
And here some screenshots:
Yes, it is some work but there is just no way to something for free when coding. In the end, we are programmers and it is up to us to do the coding. It's always better to spend the 5-10 minutes setting this up than just copy pasting some hacky code which might not continue to work in the future when Apple decides to change the view hierarchy.
It was actually more work to write this answer than implementing the separator. I as well was searching for a easy and quick solution but in the end there just wasn't a good enough one which I felt was worth using.
I hope you also feel skeptical when you see for-loops iterating over subviews of cells to hide or even remove views at runtime from the view hierarchy Apple provides you, when there is an easy, versatile, stable and future proof solution right around the corner. 7 little steps is really all you need and they are easy to understand.
This is what I finally came up with:
I was not able to find some better way than this. But it is working for me greatly. Last tried with Xcode Version 7.3.1 (7D1014). This procedure was done through storyboard.
Basically I add a UIView of 0.5 pt Height on the UITableViewCell and then set a background color for that UIView. Set parent UITableView's Separator as None.
Here is the details:
Considering you already set your UITableViewCell, custom or default. On the very first stage set the UITableView's separator as None.
Next add a UIView of 1 pt Height and set the Background as you need, on my case it is Red.
Start setting the constraints. The problem is to set the height of the UIView as 0.5 pt. This is the only Problematic issue for this workflow.
UIView with 0.5 pt Height:
Sharing the way to set 0.5 pt height of the UIView.
First(1) pin the view and then(2) set the height as 0.5. Press Enter.
Finally your UIView will look similar like following.
I was not able to set the height as 0.5 other than this way.
Here the solution. This is for static cells. If you want dynamic then just rewrite "count". Hope it helps.
extension NSObject {
var theClassName: String {
return NSStringFromClass(self.dynamicType).componentsSeparatedByString(".").last!
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.separatorStyle = .None
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
let count = tableView.numberOfRowsInSection(indexPath.section)
if ( indexPath.row != count - 1 ) {
for view in cell.subviews {
if view.theClassName == "_UITableViewCellSeparatorView" {
view.backgroundColor = UIColors.redColor()
}
}
}
}
Tried various solutions based on the cell.separatorInset = UIEdgeInsetsMake() workaround, none of them working properly.
For my iOS11 UITableViewStyleGrouped based project, this did it:
self.tableView.separatorColor = self.tableView.backgroundColor;
Based on cook's answer, but without a timer:
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if NSStringFromClass(subview.classForCoder) != "UITableViewCellContentView" && subview.frame.width == frame.width {
subview.removeFromSuperview()
}
}
Just add this in a cell's subclass and you don't need anything else. Works on iOS 12.
I had a similar issue, where I had a Grouped UITableView with custom cells, all designed with Interface Build as .xib files. The cells had white backgrounds, removed the default separators, and added my own ones. I also had custom Header Views for each section. I set the height of those Header Views to be 44 (could be anything...). There was a 1 point height view between my sections, which seemed weird. Apparently, the system adds some clear background view between the sections, even if we specify the custom height to be, say 44, and the custom Header View we return has a white (or some concrete background color). The color we see behind that clear view is the color of of the Table View's background actually. In my case, both the table views and the cells had to be of white color, and setting the background of table view to be white solved the problem (at least visually, but that's what I wanted anyway). Second solution would be to keep the Table View's style as plain, but implement the UITableView delegate method to return 2 or more sections, and also create custom headers if you need to. But that will make header views to stick to the top for a while (while scrolling), until the next section's header view gets closer to it, and then it starts going up too (and that may not be what you really want, but there may be an easy way to fix that, not sure though).
For a single-cell section, simply overriding layoutSubviews and leaving it empty does the trick! https://stackoverflow.com/a/59733571/4442627
Here's a Swift solution that works on iOS 8 and 9.
Define a protocol with a default implementation:
protocol CellSeparatorRemovable { }
extension CellSeparatorRemovable {
func removeSeparatorLinesFromCell(cell: UITableViewCell, section: Int, row: Int, indexPath: NSIndexPath) {
guard (section, row) == (indexPath.section, indexPath.row) else { return }
for view in cell.subviews where view != cell.contentView {
view.removeFromSuperview()
}
}
}
Then, wherever you want to use it, conform to the CellSeparatorRemovable protocol and call its method from …willDisplayCell…:
class SomeVC: UITableViewController, CellSeparatorRemovable {
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
removeSeparatorLinesFromCell(cell, section: 1, row: 1, indexPath: indexPath)
}
}
This is a minimal solution; you may need to refactor it if you're dealing with many cells to avoid excessive recursion and/or cell reuse issues.
You can access the view using
override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let header = view as! UITableViewHeaderFooterView
header.backgroundView . .....
Try this it will remove top separator line.
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == 0 && indexPath.section == 0) {
for (UIView *view in cell.subviews) {
if ([NSStringFromClass([view class]) containsString:#"CellSeparator"]) {
if (view.frame.origin.y == 0 && CGRectGetHeight(view.frame) < 1.01 ) { //Hide first UITableViewCellSeparatorView
NSLog(#"%s Remove View From Cell[Section:0 Row:0] for top border [%#]:%#",__FUNCTION__,NSStringFromClass([view class]),view);
view.hidden = YES; //Hide
}
}
}
}
}
iOS 10~13 only remove section head foot line.
-(void)layoutSubviews{
[super layoutSubviews];
//for iOS10~iOS13: only remove section head foot line
for (UIView * v in self.subviews) {
if ( v != self.contentView &&
(v.frame.size.width == self.frame.size.width)){
[v removeFromSuperview];
}
}
}
if wan to remove all line:
for (UIView * v in self.subviews) {
if (v != self.contentView){
[v removeFromSuperview];
}
}
override func layoutSubviews() {
super.layoutSubviews()
subviews.filter { $0 != contentView && $0.frame.width == frame.width }.first?.removeFromSuperview()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = groupTable.dequeueReusableCell(withIdentifier: "groupCell", for: indexPath) as! GroupNameCell
if indexPath.row == 2 /*user your array.count-1 (I have not used array soused 0 here)*/ {
cell.separatorInset = UIEdgeInsets(top: .zero, left: .zero, bottom: .zero, right: .greatestFiniteMagnitude)
}
return cell
}

UITableViewAutomaticDimension not working until scroll

When the Table View is first loaded, all of the visible cells are the estimatedRowHeight. As I scroll down, the cells are being automatically sized properly, and when I scroll back up the cells that were initially estimatedRowHeight are being automatically sized properly.
Once the cells are being automatically sized, they don't ever seem to go back to being the estimatedRowHeight.
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .Plain, target: nil, action: nil)
self.tableView.estimatedRowHeight = 80.0
self.tableView.rowHeight = UITableViewAutomaticDimension
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
and
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as CustomTableViewCell
// Configure the cell...
let restaurant = restaurants[indexPath.row]
cell.namelabel.text = restaurant.name
cell.locationlabel.text = restaurant.location
cell.typelabel.text = restaurant.type
cell.thumbnailImageView.image = UIImage(named: restaurant.image)
cell.thumbnailImageView.layer.cornerRadius = cell.thumbnailImageView.frame.size.width / 2
cell.thumbnailImageView.clipsToBounds = true
cell.accessoryType = restaurant.isVisited ? .Checkmark : .None
return cell
}
Thoughts on how to have the cells autoresize initially?
UPDATE: As of Xcode 7 beta 6 this is no longer an issue
Just call "reloadSections" after your Table is loaded:
self.tableView.reloadSections(NSIndexSet(indexesInRange: NSMakeRange(0, self.tableView.numberOfSections())), withRowAnimation: .None)
Or in Swift 3:
let range = Range(uncheckedBounds: (lower: 0, upper: self.tableView.numberOfSections))
self.tableView.reloadSections(IndexSet(integersIn: range), with: .none)
I ran into the same issue and found out that the accessory cell.accessoryType messes with this automatic resizing when it is not None, so it seems like a bug in Xcode at the moment.
But as #Blankarsch mentioned, calling reloadSections(..) helps to fix this issue if you need to have an accessory.
I think the answers over here have fewer side effects. Specifically adding cell.layoutIfNeeded() in tableView:cellForRowAtIndexPath:
I'm also doing what #nosov suggests as is good practice, but haven't tested if they need to be done in tandem.
just override estimatedHeightForRowAtIndexPath like this
(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
and check your autoLayout constraints in CustomTableViewCell view.
Just make sure that your labels don't have an explicit content size set at Interface Builder. They must be Automatic like the screenshot below for the Automatic Row height to work properly without the need to reload any sections up front.
If you set your constraints based on the cell itself (instead of cell content view), the table is not able to get the proper size. So, to fix this issue, your constraints must be set to the content view.
However, this is problem when your cells supports both configuration with/without accessory view. In that case, the content view gets resized according to the accessory view, and the result may not the the expected by the user. So, in this case, a solution is setting 2 constraints, one to the cell and a second one with lower priority to the cell content view.
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath;
In the function set the cell with the preferred content, in order for UITableViewAutomaticDimension to work flawlessly.
The problem is caused because the cell content is loaded in some other delegate function, so you can see that cells automatically resize to the required size.
Make sure to init your cell in tableView:cellForRowAt:. I've run into this issue even with Xcode 8 when I was setting cell content in tableView:willDisplay:forRowAt:
For me I only have this problem when using willDisplay cell to set the text of my labels
If I set the text of my labels in cellForRowAt index path, everything is fine
For me what solved it was the combination of #[David Hernandez] answer.
Removed the selection from above, and in Cell's layoutSubviews I set the preferredMaxLayoutWidth as this (change 30 to your desired left right spacing)
-(void) layoutSubviews {
[super layoutSubviews];
self.textDescriptionLabel.preferredMaxLayoutWidth = self.frame.size.width - 30;
}
Xcode 9.3, Swift 4.1
adding
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
in addition to
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
worked for me.

Resources