How to dynamically resize custom UITableViewCells in UITableView? - ios

I built a generic UITableView to which I can pass a model and a custom UITableViewCell and it's working well except for two things: the cells height and the UITableView height.
I guess these two issues are related.
I'm trying multiple things I found on SO to help with it but nothing worked out.
Here is a sample of my generic UITableView:
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
loadList(appendItems: true)
initActions()
initLayout()
}
func initLayout() {
self.view.addSubview(tableView)
self.view.addSubview(emptyView)
self.view.addSubview(loadMoreButton)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.snp.makeConstraints { (make) -> Void in
make.left.right.leading.trailing.top.equalToSuperview()
}
tableView.isScrollEnabled = false
emptyView.translatesAutoresizingMaskIntoConstraints = false
emptyView.isHidden = true
emptyView.textAlignment = .center
emptyView.snp.makeConstraints { (make) -> Void in
make.left.right.leading.trailing.equalToSuperview()
make.top.equalToSuperview().offset(8)
}
self.loadMoreButton.isHidden = true
if(self.hasLoader == true) {
self.loadMoreButton.isHidden = false
loadMoreButton.translatesAutoresizingMaskIntoConstraints = false
loadMoreButton.snp.makeConstraints { (make) -> Void in
make.centerX.equalToSuperview()
make.top.equalTo(tableView.snp.bottom)
}
}
}
Then, when my data is loaded I do:
self.tableView.reloadData()
self.tableView.snp.remakeConstraints { (make) -> Void in
make.left.right.leading.trailing.top.equalToSuperview()
if(self.hasLoader == true) {
make.bottom.equalTo(self.loadMoreButton.snp.top)
}
make.height.equalTo(self.tableView.contentSize.height)
}
Then, my tableView funcs:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return resourceList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
let currentResource = resourceList[indexPath.row]
cell.setResource(resource: currentResource) // Here I initialize my labels, UIImage, etc.
cell.isUserInteractionEnabled = true
self.cellHeight = cell.contentView.frame.size.height // Something I tried
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView.deselectRow(at: indexPath, animated: true)
let resource = resourceList[indexPath.row]
if (resource.getRouteParam() != "") {
router.setRoute(routeName: resource.getRouteName(), routeParam: resource.getRouteParam())
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return self.cellHeight ?? 200 // Here I'm trying to return an *automatic* height of my custom cell
}
To complete I can say that my cells have different sizes due to different labels length so I can't define a static height.
I also tried to add estimatedHeightForRowAt in my viewDidLoad method but it didn't work either.
When I use UITableView.automaticDimension my cells have always a height of 44.0

1. UITableView configuration
To be able to have automatically sized cells in UITableView you will need to do set the following properties on your table view:
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 200 // or other value
2. Proper AutoLayout inside of UITableViewCell
Make sure yo have auto layout setup correctly in your cells.
3. (Optional) UITableViewDelegate and estimatedHeightForRowAt
Implement this method of UITableViewDelegate to provide the table with with more accurate size, so it will correctly handle fast scrolling.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 200 // try to calculate more accurate size
}
Some stuff I noticed
This will not really work how you expect it to work so don't bother with it. Here the cells views are not even correctly laid out, so there is a very high chance you will get problematic values (like 0)
self.cellHeight = cell.contentView.frame.size.height
There should be no need to resetting the constraints after you reload data. Setting them in the viewDidLoad should be enough.
self.tableView.reloadData()
self.tableView.snp.remakeConstraints
Resources
How to make UITableViewCells auto resize to their content
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

Related

Self sizing cells height change causes jumping animation

I have setup a tableview with dynamic height cells aka UITableView.automaticDimension using Autolayout. This works fine. Now what I am trying to achieve is to change the height of cell & animate it. The issue is that when I change cell height & animate it, the animation is weirdly jumping. The jump only occurs if I scroll down a bit & then expand/collapse cells.
I have a simple table view cell. It has a label & an empty UIView with fixed height constraint. When I want to collapse/expand the cell, I simply change the constant of that height constraint to 0 or 300.
I have tried many collapsable tableview examples off the internet. All of them have this issue. One exception is https://github.com/Ramotion/folding-cell, but that uses fixed heights for cells.
I have tried quite a few options to animate the cell height change.
1-> On didSelectRow, I change the height constraint & call tableview beginUpdate & endUpdates. Doesn't solve the jump issue.
2-> Change my model & call tableView.reloadRows. Doesn't solve the jump issue.
This is screenshot of my tableview cell setup.
https://drive.google.com/open?id=12nba6cwRszxRlaSA-IhrX3X_vLZ4AWxy
A link to video of this issue:
https://drive.google.com/open?id=19Xmc0PMXT0EuHTJeeGHm4M5aPoChAtf3
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.estimatedRowHeight = 50
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedSectionHeaderHeight = 0
tableView.estimatedSectionFooterHeight = 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! OuterTableViewCell
let height: CGFloat = isCellExpanded[indexPath.row] ? 300 : 0
cell.labelText.text = "Cell Number: \(indexPath.row + 1)"
cell.buttonExpansionToggle.setImage(UIImage(named: isCellExpanded[indexPath.row] ? "arrow-down" : "arrow-right"),
for: .normal)
cell.viewContainerHeight.constant = height
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
isCellExpanded[indexPath.row] = !isCellExpanded[indexPath.row]
tableView.reloadRows(at: [indexPath], with: .automatic)
}
Another form of didSelectRow:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? OuterTableViewCell else { return }
isCellExpanded[indexPath.row] = !isCellExpanded[indexPath.row]
let height: CGFloat = self.isCellExpanded[indexPath.row] ? 300 : 0
cell.viewContainerHeight.constant = height
cell.layoutIfNeeded()
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: {
tableView.beginUpdates()
tableView.endUpdates()
// fix https://github.com/Ramotion/folding-cell/issues/169
if cell.frame.maxY > tableView.frame.maxY {
tableView.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.bottom, animated: true)
}
}, completion: nil)
}
I have also tried to call beginUpdates() & endUpdates() outside animation block, yet the issue persists.
I expect the animation to be smooth. Hope someone can help. If someone can setup a simple demo project on github that would be awesome.
Demo project link: https://gitlab.com/FahadMasoodP/nested-tableviews
Help in any form is appreciated. Thanks.
My solution is adding dynamic estimate row height. I think it is bug of UIKit. iOS 13 issue will be not occur.
First, You need add property estimateRowHeightStorage to store estimate height by indexpath
var estimateRowHeightStorage: [IndexPath:CGFloat] = [:]
Second, You need store current height of cell for use later
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
estimateRowHeightStorage[indexPath] = cell.frame.size.height
}
Final, you use estimateRowHeightStorage to set estimate row height.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if estimateRowHeightStorage[indexPath] != nil {
return estimateRowHeightStorage[indexPath]!
}
return 50
}
Run and feel.
I did found new solution in your case. If you hardfix height when expand. Only need change some thing.
Replace all code above with estimatedHeightForRowAt function
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
let height: CGFloat = isCellExpanded[indexPath.row] ? 373 : 73
return height
}

Self sizing tableview inside self sizing tableview cell

Let's say I have hierarchy like this:
*TableViewCell
**TableView
***TableViewCell
and all of them should be resizable. Did someone face this kind of problem? In past I've used many workarounds like systemLayoutSizeFitting or precalculation of height in heightForRowAt, but it always breaks some constraints, because TableViewCell has height constraint equal to estimated row height and there appear some kinds of magic behavior. Any ways to make this live?
Current workaround:
class SportCenterReviewsTableCell: UITableViewCell, MVVMView {
var tableView: SelfSizedTableView = {
let view = SelfSizedTableView(frame: .zero)
view.clipsToBounds = true
view.tableFooterView = UIView()
view.separatorStyle = .none
view.isScrollEnabled = false
view.showsVerticalScrollIndicator = false
view.estimatedRowHeight = 0
if #available(iOS 11.0, *) {
view.contentInsetAdjustmentBehavior = .never
} else {
// Fallback on earlier versions
}
return view
}()
private func markup() {
contentView.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.register(ReviewsTableViewCell.self, forCellReuseIdentifier: "Cell")
tableView.snp.makeConstraints() { make in
make.top.equalTo(seeAllButton.snp.bottom).offset(12)
make.left.equalTo(contentView.snp.left)
make.right.equalTo(contentView.snp.right)
make.bottom.lessThanOrEqualTo(contentView.snp.bottom)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ReviewsTableViewCell
cell.viewModel = viewModel.cellViewModels[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! ReviewsTableViewCell
cell.viewModel = viewModel.cellViewModels[indexPath.row]
cell.setNeedsLayout()
cell.layoutIfNeeded()
let size = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow)
return size.height
}
}
Self sizing tableView class:
class SelfSizedTableView: UITableView {
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded()
}
override var intrinsicContentSize: CGSize {
self.setNeedsLayout()
self.layoutIfNeeded()
return contentSize
}
}
This is actually not an answer to the question, but just an explanation.
(Wrote here because of the character count limitation for the comments).
The thing is that you're trying to insert a vertically scrollable view inside another vertically scrollable view. If you don't disable the nested tableview's scroll ability, you will have a glitch while scrolling, because the system wouldn't know to whom pass the scroll event (to the nested tableview, or to the parent tableview).
So in our case, you'll have to disable the "scrollable" property for the nested tableviews, hence you'll have to set the height of the nested tableview to be equal to its content size. But this way you will lose the advantages of tableview (i.e. cell reusing advantage) and it will be the same as using an actual UIScrollView. But, on the other hand, as you'll have to set the height to be equal to its content size, then there is no reason to use UIScrollView at all, you can add your nested cells to a UIStackView, and you tableview will have this hierarchy:
*TableView
**TableViewCell
***StackView
****Items
****Items
****Items
****Items
But again, the right solution is using multi-sectional tableview. Let your cells be section headers of the tableview, and let inner cells be the rows of the tableview.
here is an example of how to make a tableview inside a table view cell with automatic height for the cells.
You should use the 'ContentSizedTableView' class for the inner tableViews.
class ViewController: UIViewController {
#IBOutlet weak var outerTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
outerTableView.rowHeight = UITableView.automaticDimension
outerTableView.estimatedRowHeight = UITableView.automaticDimension
outerTableView.delegate = self
outerTableView.dataSource = self
}
}
final class ContentSizedTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
sizeToFit()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? TableTableViewCell
return cell!
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
Use xib files to simplify the hierarchy.
Get a tableView on your storyboard, and create a nib file for your tableViewCell(say CustomTableViewCell). Inside it create a tableView and again create one more tableViewCell xib file. Now, no need of setting labels into your xib file,(if you want only labels in cells and nothing else, if not, there is another way of adding constraints)
Say you have an array of text, some strings are long and some are short.
register nib file in CustomTableViewCell and extend it to use Delegate and DataSource.
register this CustomTableViewCell in ViewController.
While declaring a cell in CustomTableViewCell, just do=
cell.textLabel?.text = content
cell.textLabel?.numberOfLines = 0
Use heightForRowAt to set outer tableViewCell's height, and let the inner tableView to scroll inside.

how to perform specific action at certain table view row in swift?

I have a table view cell. I make an app for a tenant in the apartment to report the defect of the room facility. if the defect has been repaired (status: Complete), data from server will give defect.status == 2 (if defect.status == 1, still on process to be repaired), and it will show YES and NO Button like the picture above.
I want if it still on the repairment process, the view that contains "Are You satisfied" label and Yes No Button will not appear. The expected result should be like the picture below
here is the code I use to remove that satisfied or not view
extension RequestDefectVC : UITableViewDataSource {
//MARK: Table View Delegate & Datasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listDefects.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.removeFromSuperview()
}
}
return cell
}
}
but unfortunately, if that wantToRemoveView.removeFromSuperview() is triggered, it will remove all the view in all cell, even though the status is completed like picture below
I want that satisfied or not view appears if the status is complete, otherwise, it will be removed. how to do that ?
For your costumed cells are reused, removing views will cause uncertain effects. You don't actually need the specific view to be removed, only if it stays invisible.
if dataDefect.status == 2 {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.isHidden = true
}
} else {
if let wantToRemoveView = cell.commentResponseView {
wantToRemoveView.isHidden = false
}
}
Create a height constraint for that view and hook it as IBOutlet and control it's constant according to that in cellForRowAt
self.askViewH.constant = show ? 50 : 0
cell.layoutIfNeeded()
return cell
I expect you using automatic tableView cells
#Alexa289 One suggestion is that you can take heightConstraints of UIView. then create IBOutlet of your height constraints and make its constant 0 when you want to hide otherwise assign value to your static height.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
cell.viewSatisficationHeightConstraints.constant = 50
} else {
cell.viewSatisficationHeightConstraints.constant = 0
}
return cell
}
Second suggestion is that you can take label and button in view and embed stackview to your view(view contain label and button)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "defectCell", for: indexPath) as! RequestDefectCell
let dataDefect = listDefects[indexPath.row]
cell.defectData = dataDefect
if dataDefect.status == 2 {
cell.viewSatisfication.isHidden = false
} else {
cell.viewSatisfication.isHidden = true
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 40
}
you can read about UIStackView which makes hiding things easier. If you are not using stackview and hiding things the UI will not good as the space used by the hidden view will be still there. So better to use stackView when need to hide or show some view.
UIStackView : https://developer.apple.com/documentation/uikit/uistackview

In Tableview, I want to know how to i set multiline UILabel in cell last position

In UITableView, I want to know how to i set multilineUILabel (Needs to update dynamically.) in cell last position, I want to calculate for UILabel count and set multiline in tableview
#Ashu you can set tableView cell height dynamically for that particular cell.
override func viewDidLoad() {
self.tableView.estimatedRowHeight = 41.0
self.tableView.rowHeight = UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
return 100
} else {
return UITableViewAutomaticDimension
}
}
Something like code mentioned above.

reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling

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

Resources