Change UITableViewHeaderFooterView height at runtime - ios

My target is to change the UITableView section header height at runtime. Let me be more specific. Default height is 44 but on scroll before touching the top, height will be 64. I have created a subclass of UITableViewHeaderFooterView and it's uses autolayout.
I tried
var frame = sectionHeader.frame
frame.size.height = 64
sectionHeader.frame = frame
and also
tableView.sectionHeaderHeight = 64
but nothing work for me. Can anyone put some light on this problem.

when using autolayout change frame directly won't work
to change tableview section header you need implement delegate method and reload data after change
optional func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat

You don't need to subclass the footer to change I'ts height.
Check you are only setting I'ts height at: (no delegate methods)
tableView.sectionHeaderHeight = 64
Check you call or "reloadData()" on the tableView after. or
[UIView setAnimationsEnabled:NO];
[tableView beginUpdates];
[tableView endUpdates];
[UIView setAnimationsEnabled:YES];
To only change height without reloading the tableView.

I think you have to reload the entire section of the table view, since there is no public API for only updating the section header.
self.isOnTop = true // or false
tableView.reloadSections(NSIndexSet(index: 0), withRowAnimation: .None)
And change a property that modifies the return value from the aproppriate delegate method:
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
isOnTop ? 44 : 64
}
You can also try the following methods, that can update the layout without reloading:
tableView.beginUpdates()
tableView.endUpdates()
Or move to a collection view solution, that supports the tableview's behaviour: https://github.com/jamztang/CSStickyHeaderFlowLayout

Related

Auto resize font in sectionHeader

I Want to show a name and some other short informations in the sectionHeader of my Tableview.
Some of the names are very large so they don't fit, is there a way to autoresize the fontsize in the sectionHeader like in a label with:
label.adjustsFontSizeToFitWidth = true;
The easiest way would be to create a UILabel in tableView(_:viewForHeaderInSection:)
Apple Docu Discussion:
The table view uses a fixed font style for section header titles. If
you want a different font style, return a custom view (for example, a
UILabel object) in the delegate method
tableView(_:viewForHeaderInSection:) instead.
Simply follow the code for custom header view -
Setup tableView
tableView.estimatedSectionHeaderHeight = 80 // You can change this value accordingly
tableView.sectionHeaderHeight = UITableViewAutomaticDimension
Implement your custom section header and return it in the delegate method
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
switch section {
case 0:
return sectionHeader
default:
fatalError("Unreachable code")
}
}
Finally, if the contents in the header section changes while the header is presented, after the change you will have to tell the tableView to redraw itself using
func refreshTableAfterCellExpansion() {
self.tableView.beginUpdates()
self.tableView.setNeedsLayout()
self.tableView.endUpdates()
}

Issues with drawing a line during tableview reload

I have a tableview. I reload a section. My sectionheader has a line that requires to be drawn.
The positioning of this line and how it’s drawn is based on contentview’s frame
If if do tableview.reloadData() it get’s drawn correctly. Tableview header’s frame is non-zero
If I do notesTable.reloadSections([1], with: .automatic) it doesn’t get drawn correctly. Tableview header’s frame is zero!
( I need to use reloadSections because I want to animate it. reloadData() doesn’t give any animation)
so a very watered down example of my viewForHeaderInSection is:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// some code
sectionView.setupUI()
return sectionView
}
Header class:
class SectionHeaderclass: UITableViewHeaderFooterView{
func setupUI(){
let triangleViewArea = ViewWithTriangleLine(triangleCenter:Double(contentView.frame.width - 30))
}
}
‍👆 contentView.frame.width returns 0 if I animate the section reload. Why?! How can I fix this?
If you want to update header, and it has some drawing view, its better you call setNeedsDisplay() on headerView instead of reloading tableview.
Note that reloadSections(...) updates cells in the sections not the header view.

UITableView Section Header Automatic Height Not Updated Properly

I am running into an issue with automatic/dynamic UITableView section header views that contain a UILabel that wraps (numberOfLines = 0). The height is not being calculated properly, especially once you scroll the table and the views are reused. Sometimes the UILabel wraps, sometimes it is truncated, sometimes one or more of the labels are not visible, and sometimes there is extra spacing between the labels. The custom view contains a vertical UIStackView with three UILabels, once of which wraps.
A complete sample app demonstrating the issue can be found at https://github.com/outerstorm/tableviewHeaderTest.
The section header heights are set to automatic in viewDidLoad with the following:
tableView.sectionHeaderHeight = UITableViewAutomaticDimension
tableView.estimatedSectionHeaderHeight = 30.0
and also have implemented the following heightForHeaderInSection just to try to get it to work:
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableViewAutomaticDimension
}
I have also tried calling setNeedsLayout() and layoutIfNeeded() at various points to no avail. Any suggestions would be greatly appreciated.
Below is a screenshot of the behavior seen in the app. The first section is cutoff and the second section is too tall:
I have faced this kind of issue recently.
I solved it by setting preferredMaxLayoutWidth of multiline labels, i.e.
Before setting the value in labels, set their preferredMaxLayoutWidth as:
self.label1.preferredMaxLayoutWidth = self.label1.frame.size.width
self.label2.preferredMaxLayoutWidth = self.label2.frame.size.width
self.label3.preferredMaxLayoutWidth = self.label3.frame.size.width
Just add estimatedHeightForHeaderInSection function and return your estimated height. It will resolve your issue. Download your modified project from here
func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
{
return 30;
}
In general
heightForHeaderInSection
always called before
viewForHeaderInSection
when using UITableViewAutomaticDimension, sectionheader height will only calculate once unless called tableview.reloadData() manually.
In the code, you change sectionheader text everytime. the height was calculate at the first time, and doesnt change automatic.
you can change setup func to:
func setup(someNum: Int) {
let text = String(repeating: "This is where the really long text goes so that it will wrap lines appropriately", count: someNum)
mainLabel.text = text
}
and pass the section index to the function
A workaround can be hold the header views in array and then return the height of view from estimated height method
for _ in 0 ..< data.count {
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "CustomHeaderView") as! CustomHeaderView
view.setup()
headerViews.append(view)
}
func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
{
let view = headerViews[section] as? UIView
return (view?.frame.size.height)!
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return headerViews[section] as? UIView
}
From the code's documentation:
// tableView:titleForHeaderInSection: or tableView:titleForFooterInSection: if the title is not nil.
In other words. UITableViewAutomaticDimension is only intended for use if you are providing a section title using titleForHeaderInSection or titleForFooterInSection
Add
self.label1.preferredMaxLayoutWidth = self.label1.frame.size.width
self.label1.numberoflines = 0;
self.label1.linebreakmode = NSLineBreakByWordWrapping;
in awakeFromNib method
or
Kindly check this once

UITableView header view overlap cells in iOS 10

There's a simple UITableView in my app, and there's a custom view for the tableView.tableHeaderView property. When this property is set, the view has the correct size (full width, about 45px high).
[_resultHeaderView sizeToFit]; // the view as the correct frame
[_resultTableView setTableHeaderView:_resultHeaderView];
In iOS 9 and previous versions, the header displays correctly, but in iOS 10, the cells start at the same Y coordinate as my header view, so my header view appears over the first cell.
Setting these properties also have no effect:
self.edgesForExtendedLayout = UIRectEdgeNone;
self.automaticallyAdjustsScrollViewInsets = NO;
Has something changed in iOS 10 that could explain this different behavior? What would be a good solution?
Thanks
The bug was in the fact that the view was being resized automatically, so its frame height really was 0 when it was being attached to the tableView, which explains the behavior.
Setting the autoresizingMask to none fixed this bug.
_headerView.autoresizingMask = UIViewAutoresizingNone
Again, it wasn't necessary in iOS 9 and below. Hope this helps someone else.
Here is a more Swift approach
tableView.tableHeaderView?.translatesAutoresizingMaskIntoConstraints = false
Or you can do it directly in your header
myHeaderView.translatesAutoresizingMaskIntoConstraints = false
Also you must call these two delegate methods.
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return yourHeaderView
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 30 // Your preferred height
}
Just set headerView.frame before tableView.tableHeaderView = headerView

UITableView dynamic cell heights only correct after some scrolling

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.

Resources