I have a tableView and cells. The Cells are loaded from a xib and they have a label with automatic height. I need to narrow one cell if the user taps on it.
I have tried hiding - doesn't work
I have tried removeFromSuperView()- doesn't work
Is there any alternative?
When setting up your tableViewCell store the height anchor you want to update
var yourLabelHeightAnchor: NSLayoutConstraint?
private func setupLayout() {
yourLabelHeightAnchor = yourLabel.heightAnchor.constraint(equalToConstant: 50)
// Deactivate your height anchor as you want first the content to determine the height
yourLabelHeightAnchor?.isActive = false
}
When the user clicks on a cell, notify the tableView that the cell is going to change, and activate the height anchor of your cell.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: "YourTableViewCellIdentifier") as? YourCell
self.tableView.beginUpdates()
cell?.yourLabelHeightAnchor?.isActive = true
self.tableView.endUpdates()
}
Did you try to do something like this:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
var result: CGFloat
if (indexPath.row==0) {
result = 50 }
else {result = 130}
return result
}
This is just an example where height is changed for the first row. I tested on my application and it gave result like this.
I'm experiencing bugs with my application when I'm trying to download and set an image async to a cell with dynamic height.
Video of the bug: https://youtu.be/nyfjCmc0_Yk
I'm clueless: can't understand why it happens. I'm saving the cell heights for preventing jumping issues and stuff, I do even update the height of the cell after setting the image.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var post : Post
var cell : PostCell
post = Posts.shared.post(indexPath: indexPath)
// I REMOVED SOME NOT IMPORTANT PARTS OF THE CODE
// (like setting the text, etc)
if post.images.count > 0 {
// Images is attached to the post
cell.postImageView.sd_setImage(with: URL(string: appSettings.url + "/resources/img/posts/" + post.images[0]), placeholderImage: nil, options: [.avoidAutoSetImage]) { (image, error, type, url) in
if let error = error {
// placeholder
return
}
DispatchQueue.main.async {
let cgRect = image!.contentClippingRect(maxWidth: 300, maxHeight: 400)
cell.postImageView.isHidden = false
cell.postImageWidthConstraint.constant = cgRect.width
cell.postImageViewHeightConstraint.constant = cgRect.height
cell.postImageView.image = image
cell.layoutIfNeeded()
self.cellHeights[indexPath] = cell.frame.size.height
}
}
} else {
// No image is attached to the post
cell.postImageViewHeightConstraint.constant = 0
cell.postImageWidthConstraint.constant = 0
cell.postImageView.isHidden = true
}
return cell
}
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
cell.layoutIfNeeded()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if cellHeights[indexPath] != nil {
return CGFloat(Float(cellHeights[indexPath] ?? 0.0))
}
else {
return UITableViewAutomaticDimension
}
}
I've tried calling tableView.beginUpdates() tableView.endUpdates(), it fixes the problem at the beginning, but when scrolling it creates a weird bug :
https://youtu.be/932Kp0p0gfs
(random appearing small part of the image in the tableview)
And when scrolling up, it jumps to the beginning of the post.. maybe due to the incorrect height value?
What do I do wrong?
You'll surely get such kind of bug if you are to compute the height and the width of the UIImage data and assign those values as the constraint constants of the UIImageView especially in a cell.
Solution for that? Make your datasource/server have a computed width and height and avoid computing it yourself. Meaning in your each Post object, there should be a ready width and height values. That's faster and that solved my issue the same as yours (see this cute personal quick project of mine: https://itunes.apple.com/us/app/catlitter-daily-dose-of-cats/id1366205944?ls=1&mt=8)
Also, I've learned that from some public APIs provided by some established companies like Facebook (each images have its computed sizes ready).
I hope this helps.
I got two cells in my UITableView. One is a custom UITableViewCell and the other is a cell with a UITextView inside and called the type TextViewCell.
Because they are static I the cells are loaded in viewDidLoad method from a xib:
textCell = Bundle.main.loadNibNamed(String(describing: TextViewCell.self),
owner: self, options: nil)?.first! as! TextViewCell
ratingCell = Bundle.main.loadNibNamed(String(describing: RatingCell.self),
owner: self, options: nil)?.first! as! RatingCell
Now I try to change the height with the the heightForRowAt delegate:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 1 {
return textCell.textView.contentSize.height
}
return ratingCell.ratingView.frame.height
}
I disabled scrolling on the UITextView but the cell is not resizing properly. In fact the cells gets smaller.
The constraints of the TextViewCell look like this:
Any suggestions?
I think you need to use self-sizing UITableViewCell. Replace your current implementation of heightForRowAt with the following:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
Now height of cells in your UITableView object will be calculated automatically based on constraints.
Also, add some estimated value for row height:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 100.0 // You can set any other value, it's up to you
}
Now you will see that the UITextView view fills the whole UITableViewCell cell.
You should get the height of UITextView in heightForRowAt and return this height as cell height. See example below
let lblDescLong = UITextView()
lblDescLong.textAlignment = .left
lblDescLong.text = “your text for text view”
lblDescLong.font = YourFont(size: 12)
let newSize = lblDescLong.sizeThatFits(CGSize(width: widthForTextView, height: CGFloat.greatestFiniteMagnitude))
return newSize.height
I have a UITableView. In my tableview l I want to dynamically add another view into my cell and expand the cell height. I have seen some examples but those are with UILabels. UILabels can changes the height according to the text height. But how can I manually add another view and expand those cells?
Please help me
Thanks
If you use the manually calculate the cell height,its easy,after you add another view,you calculate the height,then call reloadData or reloadRowsAtIndexPaths,and return the height in tableview:heightForRowAtIndexPath function.
If you use the auto calculate the cell height.you should let system know how to calculate,you should set the autolayout clearly and completely.You should add the another view`s left、right、top、bottom constraint and the system will auto canculate the height.So the cells will Be expanded.
You can set cell height by override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
Implement these two UITableViewDelegate methods:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
// return estimatedHeight
}
View in your dynamic cell needs to have top, bottom, trailing, leading and height constraints. Setting view height constraint constant will set cell height according to view height.
You can have view height constraint property in your dynamic cell:
#IBOutlet weak var customViewHeightConstraint: NSLayoutConstraint!
Constraint constant value can be changed in cellForRowAt:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellId", for: indexPath) as! Cell
switch indexPath.row {
case 0:
cell.customViewHeightConstraint.constant = 100
case 1:
cell.customViewHeightConstraint.constant = 200
case 2:
cell.customViewHeightConstraint.constant = 300
case 3:
cell.customViewHeightConstraint.constant = 400
default:
cell.customViewHeightConstraint.constant = 100
}
return cell
}
I feel like this might be a common issue and was wondering if there was any common solution to it.
Basically, my UITableView has dynamic cell heights for every cell. If I am not at the top of the UITableView and I tableView.reloadData(), scrolling up becomes jumpy.
I believe this is due to the fact that because I reloaded data, as I'm scrolling up, the UITableView is recalculating the height for each cell coming into visibility. How do I mitigate that, or how do I only reloadData from a certain IndexPath to the end of the UITableView?
Further, when I do manage to scroll all the way to the top, I can scroll back down and then up, no problem with no jumping. This is most likely because the UITableViewCell heights were already calculated.
To prevent jumping you should save heights of cells when they loads and give exact value in tableView:estimatedHeightForRowAtIndexPath:
Swift:
var cellHeights = [IndexPath: CGFloat]()
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? UITableView.automaticDimension
}
Objective C:
// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary = #{}.mutableCopy;
// declare table dynamic row height and create correct constraints in cells
tableView.rowHeight = UITableViewAutomaticDimension;
// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
[cellHeightsDictionary setObject:#(cell.frame.size.height) forKey:indexPath];
}
// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
if (height) return height.doubleValue;
return UITableViewAutomaticDimension;
}
Swift 3 version of accepted answer.
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 70.0
}
The jump is because of a bad estimated height. The more the estimatedRowHeight differs from the actual height the more the table may jump when it is reloaded especially the further down it has been scrolled. This is because the table's estimated size radically differs from its actual size, forcing the table to adjust its content size and offset.
So the estimated height shouldn't be a random value but close to what you think the height is going to be. I have also experienced when i set UITableViewAutomaticDimension
if your cells are same type then
func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 100//close to your cell height
}
if you have variety of cells in different sections then I think the better place is
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
//return different sizes for different cells if you need to
return 100
}
#Igor answer is working fine in this case, Swift-4 code of it.
// declaration & initialization
var cellHeightsDictionary: [IndexPath: CGFloat] = [:]
in following methods of UITableViewDelegate
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// print("Cell height: \(cell.frame.size.height)")
self.cellHeightsDictionary[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = self.cellHeightsDictionary[indexPath] {
return height
}
return UITableView.automaticDimension
}
I have tried all the workarounds above, but nothing worked.
After spending hours and going through all the possible frustrations, figured out a way to fix this. This solution is a life savior! Worked like a charm!
Swift 4
let lastContentOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastContentOffset, animated: false)
I added it as an extension, to make the code look cleaner and avoid writing all these lines every time I want to reload.
extension UITableView {
func reloadWithoutAnimation() {
let lastScrollOffset = contentOffset
beginUpdates()
endUpdates()
layer.removeAllAnimations()
setContentOffset(lastScrollOffset, animated: false)
}
}
finally ..
tableView.reloadWithoutAnimation()
OR you could actually add these line in your UITableViewCell awakeFromNib() method
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
and do normal reloadData()
I use more ways how to fix it:
For view controller:
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 70.0
}
as the extension for UITableView
extension UITableView {
func reloadSectionWithoutAnimation(section: Int) {
UIView.performWithoutAnimation {
let offset = self.contentOffset
self.reloadSections(IndexSet(integer: section), with: .none)
self.contentOffset = offset
}
}
}
The result is
tableView.reloadSectionWithoutAnimation(section: indexPath.section)
I ran into this today and observed:
It's iOS 8 only, indeed.
Overridding cellForRowAtIndexPath doesn't help.
The fix was actually pretty simple:
Override estimatedHeightForRowAtIndexPath and make sure it returns the correct values.
With this, all weird jittering and jumping around in my UITableViews has stopped.
NOTE: I actually know the size of my cells. There are only two possible values. If your cells are truly variable-sized, then you might want to cache the cell.bounds.size.height from tableView:willDisplayCell:forRowAtIndexPath:
You can in fact reload only certain rows by using reloadRowsAtIndexPaths, ex:
tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)
But, in general, you could also animate table cell height changes like so:
tableView.beginUpdates()
tableView.endUpdates()
Overriding the estimatedHeightForRowAtIndexPath method with an high value, for example 300f
This should fix the problem :)
Here's a bit shorter version:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.cellHeightsDictionary[indexPath] ?? UITableViewAutomaticDimension
}
There is a bug which I believe was introduced in iOS11.
That is when you do a reload the tableView contentOffSet gets unexpectedly altered. In fact contentOffset should not change after a reload. It tends to happen due to miscalculations of UITableViewAutomaticDimension
You have to save your contentOffSet and set it back to your saved value after your reload is finished.
func reloadTableOnMain(with offset: CGPoint = CGPoint.zero){
DispatchQueue.main.async { [weak self] () in
self?.tableView.reloadData()
self?.tableView.layoutIfNeeded()
self?.tableView.contentOffset = offset
}
}
How you use it?
someFunctionThatMakesChangesToYourDatasource()
let offset = tableview.contentOffset
reloadTableOnMain(with: offset)
This answer was derived from here
This one worked for me in Swift4:
extension UITableView {
func reloadWithoutAnimation() {
let lastScrollOffset = contentOffset
reloadData()
layoutIfNeeded()
setContentOffset(lastScrollOffset, animated: false)
}
}
One of the approach to solve this problem that I found is
CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock {
UIView.setAnimationsEnabled(true)
}
tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()
None of these solutions worked for me. Here's what I did with Swift 4 & Xcode 10.1...
In viewDidLoad(), declare table dynamic row height and create correct constraints in cells...
tableView.rowHeight = UITableView.automaticDimension
Also in viewDidLoad(), register all your tableView cell nibs to tableview like this:
tableView.register(UINib(nibName: "YourTableViewCell", bundle: nil), forCellReuseIdentifier: "YourTableViewCell")
tableView.register(UINib(nibName: "YourSecondTableViewCell", bundle: nil), forCellReuseIdentifier: "YourSecondTableViewCell")
tableView.register(UINib(nibName: "YourThirdTableViewCell", bundle: nil), forCellReuseIdentifier: "YourThirdTableViewCell")
In tableView heightForRowAt, return height equal to each cell's height at indexPath.row...
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
let cell = Bundle.main.loadNibNamed("YourTableViewCell", owner: self, options: nil)?.first as! YourTableViewCell
return cell.layer.frame.height
} else if indexPath.row == 1 {
let cell = Bundle.main.loadNibNamed("YourSecondTableViewCell", owner: self, options: nil)?.first as! YourSecondTableViewCell
return cell.layer.frame.height
} else {
let cell = Bundle.main.loadNibNamed("YourThirdTableViewCell", owner: self, options: nil)?.first as! YourThirdTableViewCell
return cell.layer.frame.height
}
}
Now give an estimated row height for each cell in tableView estimatedHeightForRowAt. Be accurate as you can...
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
return 400 // or whatever YourTableViewCell's height is
} else if indexPath.row == 1 {
return 231 // or whatever YourSecondTableViewCell's height is
} else {
return 216 // or whatever YourThirdTableViewCell's height is
}
}
That should work...
I didn't need to save and set contentOffset when calling tableView.reloadData()
I have 2 different cell heights.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
return Helper.makeDeviceSpecificCommonSize(cellHeight)
}
After I added estimatedHeightForRowAt, there was no more jumping.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
return Helper.makeDeviceSpecificCommonSize(cellHeight)
}
For me the working solution is
UIView.setAnimationsEnabled(false)
tableView.performBatchUpdates { [weak self] in
self?.tableView.reloadRows(at: [indexPath], with: .none)
} completion: { [weak self] _ in
UIView.setAnimationsEnabled(true)
self?.tableView.scrollToRow(at: indexPath, at: .top, animated: true) // remove if you don't need to scroll
}
I have expandable cells.
Try to call cell.layoutSubviews() before returning cell in func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. It's known bug in iOS8.
You can use the following in ViewDidLoad()
tableView.estimatedRowHeight = 0 // if have just tableViewCells <br/>
// use this if you have tableview Header/footer <br/>
tableView.estimatedSectionFooterHeight = 0 <br/>
tableView.estimatedSectionHeaderHeight = 0
I had this jumping behavior and I initially was able to mitigate it by setting the exact estimated header height (because I only had 1 possible header view), however the jumps then started to happen inside the headers specifically, not affecting the whole table anymore.
Following the answers here, I had the clue that it was related to animations, so I found that the table view was inside a stack view, and sometimes we'd call stackView.layoutIfNeeded() inside an animation block. My final solution was to make sure this call doesn't happen unless "really" needed, because layout "if needed" had visual behaviors in that context even when "not needed".
I had the same issue. I had pagination and reloading data without animation but it did not help the scroll to prevent jumping. I have different size of IPhones, the scroll was not jumpy on iphone8 but it was jumpy on iphone7+
I applied following changes on viewDidLoad function:
self.myTableView.estimatedRowHeight = 0.0
self.myTableView.estimatedSectionFooterHeight = 0
self.myTableView.estimatedSectionHeaderHeight = 0
and my problem solved. I hope it helps you too.
For me, it worked with "heightForRowAt"
extension APICallURLSessionViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
print("Inside heightForRowAt")
return 130.50
}
}
Actually I found if you use reloadRows causing a jump problem. Then you should try to use reloadSections like this:
UIView.performWithoutAnimation {
tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}