Identify when the UITableView loads the visible set of rows in iOS - ios

I am looking for the event that gets fired when the tableview loads the rows to fit the screen. I know tableview loads only the number of rows that fit the screen. I want to execute a set of code when the rows that fit the screen are loaded.
Any pointers on how to determine this?

I think you'd need to implement UITableViewDelegate and override the WillDisplay(UITableView, UITableViewCell, NSIndexPath) method. Be sure to set the delegate of your tableview to the class that implements UITableViewDelegate.
Unfortunately WillDisplay is called per cell, not per row.

I am not good at c#, so please translate this from Swift. I've added another solution down below which is, sort of a manual calculation of visible rows in tableview.
The Recommended Solution:
var isFirstTime:Bool = true
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let indexPaths = tableView.indexPathsForVisibleRows {
if indexPath == indexPaths.last {
if isFirstTime {
self.visibleCellsLoaded()
}
isFirstTime = false
}
}
}
The isFirstTime (Very important) flag will restrict your specific "set of code" (self.visibleCellsLoaded()) to execute only once. You can remove it if you want it to be executed every time you scroll - which apparently negates the purpose of your question.
Another solution that also works:
Here we manually calculate and get what tableView.indexPathsForVisibleRows returns us (Result is mostly similar to the previous method)
fileprivate func getLastVisibleIndexPath() -> IndexPath {
//This samples the first indexPath only, so this works only with rows that have static height, not with UITableViewAutomaticDimension.
let firstIndexPath = IndexPath.init(row: 0, section: 0)
let tableViewHeight = self.tableView.bounds.height
let rowHeight = tableView.rectForRow(at: firstIndexPath).size.height
let numberOfVisibleRows = tableViewHeight / rowHeight
return IndexPath.init(row: Int(numberOfVisibleRows - 1), section: 0)
}
var isFirstTime:Bool = true
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath == getLastVisibleIndexPath() {
if isFirstTime {
self.visibleCellsLoaded()
}
isFirstTime = false
}
}

Related

swift messaging app, uitableview not responding correctly to keyboard?

A similar question is this, except I don't have estimated row heights, I store the actual heights into a dictionary, and either use those, or use UITableViewAutomaticDimension.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeightDict[indexPath.row] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = cellHeightDict[indexPath.row] {
return height
} else {
return UITableViewAutomaticDimension
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = cellHeightDict[indexPath.row] {
return height
} else {
return UITableViewAutomaticDimension
}
}
So I'm making a messaging app in Swift 4 right now. When the user shows the keyboard, I want the messages to shift up with the keyboard. So in order to do this, I have a variable keyboardHeight which correctly gets the height of the keyboard:
let keyboardHeight: CGFloat = ((userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height)!
I change the height of the table view by keyboardHeight:
self.messageTableViewHeight.constant -= keyboardHeight
So the users can see all of the messages. And to shift up the messages with the keyboard animation, I changed the contentOffset of the table view:
self.messageTableView.contentOffset.y += keyboardHeight
All of this is in a UIView.animationWithDuration, and I call self.view.layoutIfNeeded() after, so the shifting and everything works fine. But, when I send a message without scrolling to the bottom, the messageTableView content jumps down??? And it seems to shift down by keyboardHeight, at least when I eye it. I am using messageTableView.reloadData(). Here are some images.
Before message sent
It jumps up in between the "Hi" and "Testing" messages.
After message sent
As soon as you reload the data of the tableview messageTableView.reloadData(). Every row is reloaded but it is not able to calculate the actual content size somehow. You can try reloading the data in the main queue.
DispatchQueue.main.async {
messageTableView.reloadData()
}
If it still doesn't works, you can add one more thing to the above code. Suppose you have an array of messages in your class. var messages then you can use this code to display data correctly in the tableview.
DispatchQueue.main.async {
messageTableView.reloadData()
if messages.count > 0 {
let indexPath = IndexPath(row: messages.count - 1, section: 0)
self.messageTableView.scrollToItem(at: indexPath, at: .bottom, animated: true)
}
}
Hope this helps. Happy coding.

Swift What is the best way to replace content in TableViewCell

I want to do something like in the GIF
I tried 2 ways, one was hiding the elements on selecting the row and showing others, but that's not really elegant and doesn't work very well
and second was creating 2 views, one with labels, another with buttons, adding them as subviews to cell.contentView but that caused some issues with other cells as they were displaying wrong data. How can I recreate something like this?
I think something like this would work:
Use 2 different UITableViewCells: add them to the table view in your storyboard and design them separately, also you can use 2 different UITableViewCell subclasses for them
Have an array in the tableview's datasource class that will define the type of the cell from each row (e.g. the simplest solution would be an array of integers, with values: 0 representing the first cell, 1 representing the second cell)
Initialise that array with 0s for each row
In tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell :
if cellTypes[indexPath.row] == 0 --> return a cell of first type
if cellTypes[indexPath.row] == 1 --> return a cell of the second type
In tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) :
switch the cell type in the array
reload the row with animation, e.g. you can use .fade or .left or .right etc.
tableView.reloadRows(at: [indexPath], with: .fade)
EDIT: Your solution is also a good one, but it can cause problems when the cells are dequeued, so if a cell with the wrong subviews is dequeued then you need to switch the subviews back in the cellForRowAt indexPath datasource method.
EDIT2: I took some time and I have tried my solution in Xcode. Here is the code of my tableview controller:
class TableViewController: UITableViewController {
private var cellTypes: [Int] = [1, 1, 1, 1, 1, 1]
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.cellTypes.count
}
public override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 56.0
}
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if self.cellTypes[indexPath.row] == 1 {
return tableView.dequeueReusableCell(withIdentifier: "Cell1", for: indexPath)[![enter image description here][1]][1]
} else {
return tableView.dequeueReusableCell(withIdentifier: "Cell2", for: indexPath)
}
}
public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if self.cellTypes[indexPath.row] == 1 {
self.cellTypes[indexPath.row] = 2
tableView.reloadRows(at: [indexPath], with: .fade)
} else {
self.cellTypes[indexPath.row] = 1
tableView.reloadRows(at: [indexPath], with: .right)
}
}
}
And here is how it is working in the iOS simulator:
I believe you are on the right track about creating 2 separate views inside the cell; one for showing 3 buttons (Play Now, Play Next etc.) and, one for showing the song's details (song name, singer name etc.).
In order not to mess with frames or constraints (in case you are using Autolayout), the main trick here is to create a snapshot of the view containing the buttons and move it to the end of the cell.
As I said above, you should have 2 separate views. I'll call them:
infoView: View that has 2 labels showing the song's and the singer's name.
actionsView: View that has 3 buttons for play actions. (Now, Next, Last etc.)
Here are things that you should do when user taps on a cell:
Check if cell is not selected. If it is not, then hide infoView and show actionView.
If cell is selected:
Deselect the cell.
Create a snapshot out of actionsView, set its frame accordingly so it'll shadow the real actionsView.
Set actionView's isHidden property to true.
Set infoView's isHidden property to false.
Set frame.origin.x value of the snapshot to contentView's maxX in an animation block so it'll move to the right side of the cell smoothly.
At the end of the animation, remove the snapshot from view hierarchy.
I've created a cell class and defined a method that executes those steps:
public class SongCell: UITableViewCell {
#IBOutlet fileprivate weak var infoView: UIView!
#IBOutlet fileprivate weak var actionsView: UIView!
...
public func showActions(_ show: Bool) {
switch show {
case true:
infoView.isHidden = true
actionsView.isHidden = false
case false:
if let snapshot = actionsView.snapshotView(afterScreenUpdates: true) {
snapshot.frame = actionsView.frame
contentView.addSubview(snapshot)
actionsView.isHidden = true
infoView.isHidden = false
UIView.animate(withDuration: 0.25, animations: {
snapshot.frame.origin.x = self.contentView.frame.maxX
}, completion: { _ in
snapshot.removeFromSuperview()
})
}
else {
infoView.isHidden = false
actionsView.isHidden = true
}
}
}
}
Here is how it looks on my simulator:
You can download the project from here.

Selecting a cell and changing the alpha of all cells in tableView - Swift

I'm working on a project that needs something I never imagined to have. The app for iOS is directed to iPad due to size. To that question, I made a small prototype to show one of the parties to detail better.
This is a tableView where the functions and actions will happen.
And this is the Swift code:
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let data:[String] = ["Row 0","Row 1", "Row 2","Row 3","Row 4","Row 5","Row 6"]
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:Cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! Cell
cell.labelText.text = self.data[indexPath.row]
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.cellForRowAtIndexPath(indexPath)
cell!.alpha = 0.5
}
}
What the app to do exactly?
Well, when a row is selected, the rows below it need to stay with the Alpha equal to 0.5.
Examples:
I touched the row 3
Action:
Row 1, Row 2 and Row 3 will keep the Alpha equal to 1.0
Row 4, Row 5 and Row 6 will keep the Alpha equal to 0.5
I touched in row 4
Action:
Row 1, Row 2, Row 3 and Row 4 will keep the Alpha equal to 1.0
Row 5, Row 6 will keep the Alpha equal to 0.5
.
.
.
Can someone help me?
You'd want to set the alpha value in the cellForRowAtIndexPath, then simply reload that row when its tapped. This should preserve the alpha for that cell and set alpha to 1 on every other cell, even if the the user scrolls the cell offscreen.
var selectedIndexPath:NSIndexPath?
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
if let selectedIndexPath = self.selectedIndexPath where indexPath.row == selectedIndexPath.row {
cell.alpha = 0.5
} else {
cell.alpha = 1
}
cell.labelText.text = self.data[indexPath.row]
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.selectedIndexPath = indexPath
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
Make an integer variable "selectedCell", and in didSelectRowAtIndexPath, set it equal to indexPath.row, then tableView.reloadData()
In cellForRowAtIndexPath simply use an if statement to determine if indexPath.row is greater than "selectedCell". If so then set the alpha value to 0.5, otherwise set it to 1.0.
It is generally a good practice to change cell properties in cellForRowAtIndexPath, because every time it is called you risk overwriting changes you make to cells elsewhere in the code. Hope this helped.
Hi I am proposing below solution, please consider
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let data:[String] = ["Row 0","Row 1", "Row 2","Row 3","Row 4","Row 5","Row 6"]
var rowSelected:Int
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:Cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! Cell
cell.labelText.text = self.data[indexPath.row]
if rowSelected <= indexPath.row
cell!.alpha = 0.5;
else
cell!.alpha = 1.0;
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = self.tableView.cellForRowAtIndexPath(indexPath)
rowSelected = indexPath.row
tableView.reloadData()
}
}
I'm not sure if you are describing what you want it to do or the anormal behaviour.
There are two possibilities:
1) If you want the selection related alpha to render on a single row, you may want to override didDeselectRowAtIndexPath and set the alpha to 1.0 there.
2) You need to explicitly set the alpha after getting the cell from dequeueReusableCellWithIdentifier because you are not guaranteed to get a fresh cell instance every time.
The tableview will call cellForRowAtIndexPath for a variety of reasons even after showing it for the first time. What is probably happening is that cellForRowAtIndexPath is called for rows that have been deselected and dequeueReusableCellWithIdentifier may or may not reuse an existing cell object. When it reuses a cell object, properties are not reset.

Display cells of tableview gradually using autolayout?

I have a tableview inside my UIViewController to display comments. The height of this tableview depends on the number and the size of comments. The cells are dynamic.
I use autolayout, so my tableview has a height constraint. I set this constraint programmatically :
override func viewDidLayoutSubviews() {
self.heightCommentsTableView.constant = self.commentsTableView.contentSize.height + 50
}
It works if I display all the comments at once.
BUT, I would like display the comments 5 per 5, using this method : load more for UITableView in swift because of performance issue to display my view
(all my comments are loaded before pushing the view)
I noticed when I set my constraint, it calls cellForRowAtIndexPath for all the comments, not only the first 5.
I don't know how to do.
EDIT
var allCommentsArray: NSMutableArray = []
var elements: NSMutableArray = []
var range = 5
var currentPage = 0
var nextpage = 0
override func viewDidLoad() {
super.viewDidLoad()
allCommentsArray = NSMutableArray(array: comments)
elements.addObjectsFromArray(allCommentsArray.subarrayWithRange(NSMakeRange(0, range)))
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return comments.count
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
self.nextpage = self.elements.count - 5
if indexPath.row == nextpage {
self.currentPage++
self.nextpage = self.elements.count - 5
self.elements.addObjectsFromArray(self.allCommentsArray.subarrayWithRange(NSMakeRange(self.currentPage, self.range)))
tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CommentCustomTableViewCell", forIndexPath: indexPath) as! CommentCustomTableViewCell
self.configureCell(cell, atIndexPath: indexPath)
return cell
}
The table view will call tableView(_:cellForRowAtIndexPath:) for every visible cell it is about to create, the amount of it will determine based on how many sections and how many cells in each section. You let the table vie know these amounts by implementing the tableView(_:numberOfRowsInSection:) and numberOfSectionsInTableView(_:) methods of UITableViewDataSource. So, if you want to control how many cells could possibly be created and visible at a given time, you'd have to manage that state in your data source by adding and removing according to whatever logic you desire. In the answer that you linked, you can see that he is called elements.addObjectsFromArray to progressively add more elements in batches.

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