My app has chat functionality and I'm feeding in new messages like this:
[self.tableView beginUpdates];
[messages addObject:msg];
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:messages.count - 1 inSection:1]] withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:messages.count - 1 inSection:1] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
However, my table view "jumps" weirdly when I'm adding a new message (either sending and receiving, result is the same in both):
Why am I getting this weird "jump"?
OK, I figured it out. As you say, the problem has to do with auto-sizing cells. I used two tricks to make things work (my code is in Swift, but it should be easy to translate back to ObjC):
1) Wait for the table animation to finish before taking further action. This can be done by enclosing the code that updates the table within a block between CATransaction.begin() and CATransaction.commit(). I set the completion block on CATransaction -- that code will run after the animation is finished.
2) Force the table view to render the cell before scrolling to the bottom. I do it by increasing the table's contentOffset by a small amount. That causes the newly inserted cell to get dequeued, and its height gets calculated. Once that scroll is done (I wait for it to finish using the method (1) above), I finally call tableView.scrollToRowAtIndexPath.
Here's the code:
override func viewDidLoad() {
super.viewDidLoad()
// Use auto-sizing for rows
tableView.estimatedRowHeight = 40
tableView.rowHeight = UITableViewAutomaticDimension
tableView.dataSource = self
}
func chatManager(chatManager: ChatManager, didAddMessage message: ChatMessage) {
messages.append(message)
let indexPathToInsert = NSIndexPath(forRow: messages.count-1, inSection: 0)
CATransaction.begin()
CATransaction.setCompletionBlock({ () -> Void in
// This block runs after the animations between CATransaction.begin
// and CATransaction.commit are finished.
self.scrollToLastMessage()
})
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathToInsert], withRowAnimation: .Bottom)
tableView.endUpdates()
CATransaction.commit()
}
func scrollToLastMessage() {
let bottomRow = tableView.numberOfRowsInSection(0) - 1
let bottomMessageIndex = NSIndexPath(forRow: bottomRow, inSection: 0)
guard messages.count > 0
else { return }
CATransaction.begin()
CATransaction.setCompletionBlock({ () -> Void in
// Now we can scroll to the last row!
self.tableView.scrollToRowAtIndexPath(bottomMessageIndex, atScrollPosition: .Bottom, animated: true)
})
// scroll down by 1 point: this causes the newly added cell to be dequeued and rendered.
let contentOffset = tableView.contentOffset.y
let newContentOffset = CGPointMake(0, contentOffset + 1)
tableView.setContentOffset(newContentOffset, animated: true)
CATransaction.commit()
}
Change UITableViewRowAnimationBottom to UITableViewRowAnimationNone and try
Try This!
UITableViewRowAnimation rowAnimation = UITableViewRowAnimationTop;
UITableViewScrollPosition scrollPosition = UITableViewScrollPositionTop;
[self.tableView beginUpdates];
[messages addObject:msg];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:rowAnimation];
[self.tableView endUpdates];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:YES];
// Fixes the cell from blinking (because of the transform, when using translucent cells)
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
For Swift 3 and 4
for scroll down to bottom of table View automatically when add new item in the table view just in tableView function add following line its works me
tableView.scrollToRow(at: IndexPath, at: .bottom, animated: true)
for example
in my case I have only one section so 0 is use for section and I have list of orderItems so for last index I use orderItems.count - 1
tableView.scrollToRow(at: [0, orderItems.count - 1], at: .bottom, animated: true)
I've just found out that on ios 11 this problem no longer exists. So there's no longer a content jump when adding a row to a table view and then scrolling to it with scrollToRow(at:) .
Also, on ios 10 calling scrollToRowAtIndexPath with animated=false fixes the content jump
Related
Can Somebody please tell me, How to show the middle element of array in the collection view when the app is launched and other elements are show in the left and right side of this middle element?
In viewDidLayoutSubviews method scroll your collection view to that particular IndexPath. And for do it for first time when launch use bool variable.
var isFirstTime = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if isFirstTime {
isFirstTime = false
let selectedIndex = IndexPath(item: 0, section: 0)
self.collectionView.scrollToItem(at: selectedIndex, at: .centeredHorizontally, animated: false)
}
}
You can use [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:(myArr.count/2) inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
Change the section in index path accordingly.
I have a TableView with dynamic cell size :
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 20
I have cells with texts inside and, of course, the text Height can change in function of its content.
Sometimes I have only one line, sometimes I have more, it means sometimes my cells does 20 height, sometimes more.
I have an issue when I try to reload my tableview datas and scroll to the top.
This is what I do :
myTableViewDatas = newDatas
tableView.setContentOffset(CGPoint.zero, animated: false)
tableView.reloadData()
It is hard to show you this case but It doesn't scroll to Y = 0, it scrolls to Y = 100 or something like that. Because my cell size changes in function of the content to display.
If I remove dynamic size and do :
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 20
}
And still scroll to the top with :
myTableViewDatas = newDatas
tableView.setContentOffset(CGPoint.zero, animated: false)
tableView.reloadData()
==> This is working, I scroll to Y = 0
I think I tried anything :
scrollToRow
scrollRectToVisible
scrollsToTop
I still have the issue.
The only way this is working is if I delay the reloadData :
myTableViewDatas = newDatas
tableView.setContentOffset(CGPoint.zero, animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
tableView.reloadData()
}
This is working but It creates a "glitch" => It displays new datas then automatically scroll to top, this is disturbing for the user.
The other solution is to use "reloadSections" :
myTableViewDatas = newDatas
tableView.setContentOffset(CGPoint.zero, animated: false)
tableView.reloadSections(IndexSet(integer: 0), with: .none) // I have only one section
It works too but it is also creating a "glitch", this is like TableView is reloaded with an animation (even if I set .none) where cells displayed are reduced / enlarged in function of new datas.
I really can't find a "proper" solution to do this, does anyone as already encountered this issue ? TY
Well, sometimes you search complicated solutions and It is simple in fact.
I did :
- reload first
- scrollToIndexPath 0 second
-> It works
myTableViewDatas = newDatas
tableView.reloadData()
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
I also have the same issue. [UITableView reloadData] doesn't work how we expected.
It doesn't layout tableView from the cleanslate.
Reloads everything from scratch. Redisplays visible rows. Note that this will cause any existing drop placeholder rows to be removed.
It says redisplays visible rows. So this is what I did.
data = nil; // clear data.
[tableView reloadData]; // reloadd tableView so make it empty.
[tableView setNeedsLayout]; // layout tableView.
[tableView layoutIfNeeded];
data = the data; // set the data you want to display.
[tableView reloadData]; // reload tableView
What is the proper way of scrolling a UITableView to the top when using estimated cell heights by implementing tableView:estimatedHeightForRowAtIndexPath:?
I noticed that the usual method does not necessarily scroll to the top if there is enough estimation error.
[self.tableView setContentOffset:CGPointMake(0, 0 - self.tableView.contentInset.top) animated:animated];
I came across a similar issue (I wasn't trying to scroll the tableview to the top manually but the view wasn't scrolling correctly when tapping the status bar).
The only way I've come up with to fix this is to ensure in your tableView:estimatedHeightForRowAtIndexPath: method you return the actual height if you know it.
My implementation caches the results of calls to tableView:heightForRowAtIndexPath: for efficiency , so I'm simply looking up this cache in my estimations to see if I already know the real height.
I think the issue comes from tableView:estimatedHeightForRowAtIndexPath: being called in preference over tableView:heightForRowAtIndexPath: even when scrolling upwards over cells that have already been rendered. Just a guess though.
How about tableView.scrollToRow? Solved the issue for me.
Swift 3 example:
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
how about this snippet code
[UIView animateWithDuration:0.0f animations:^{
[_tableView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:NO]; //1
} completion:^(BOOL finished) {
_tableView.contentOffset = CGPointZero; //2
}];
scroll to offset that calculated by estimatedHeightForRowAtIndexPath
setContentOffsetZero
inspiration from https://github.com/caoimghgin/TableViewCellWithAutoLayout/issues/13
let point = { () -> CGPoint in
if #available(iOS 11.0, *) {
return CGPoint(x: -tableView.adjustedContentInset.left, y: -tableView.adjustedContentInset.top)
}
return CGPoint(x: -tableView.contentInset.left, y: -tableView.contentInset.top)
}()
for section in (0..<tableView.numberOfSections) {
if 0 < tableView.numberOfRows(inSection: section) {
// Find the cell at the top and scroll to the corresponding location
tableView.scrollToRow(at: IndexPath(row: 0, section: section),
at: .none,
animated: true)
if tableView.tableHeaderView != nil {
// If tableHeaderView != nil then scroll to the top after the scroll animation ends
CATransaction.setCompletionBlock {
tableView.setContentOffset(point, animated: true)
}
}
return
}
}
tableView.setContentOffset(point, animated: true)
In IOS6 I have the following code to scroll to the top of a UITableView
[tableView setContentOffset:CGPointZero animated:YES];
In IOS7 this doesn't work anymore. The table view isn't scrolled completely to the top (but almost).
In iOS7, whole screen UITableView and UIScrollView components, by default, adjust content and scroll indicator insets to just make everything work. However, as you've noticed CGPointZero no longer represents the content offset that takes you to the visual "top".
Use this instead:
self.tableView.contentOffset = CGPointMake(0, 0 - self.tableView.contentInset.top);
Here, you don't have to worry about if you have sections or rows. You also don't tell the Table View to target the first row, and then wonder why it didn't show all of your very tall table header view, etc.
Try this:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
Based on the accepted answer from #Markus Johansson, here is the Swift code version:
func scrollToTop() {
if (self.tableView.numberOfSections > 0 ) {
let top = NSIndexPath(row: Foundation.NSNotFound, section: 0)
self.tableView.scrollToRow(at: top as IndexPath, at: .top, animated: true);
}
}
By the help from some other answers here I managed to get it working. To avoid a crash I must first check that there are some sections. NsNotFound can be used as a row index if the first section has no rows. Hopefully this should be a generic function to be placed in a UITableViewController:
-(void) scrollToTop
{
if ([self numberOfSectionsInTableView:self.tableView] > 0)
{
NSIndexPath* top = [NSIndexPath indexPathForRow:NSNotFound inSection:0];
[self.tableView scrollToRowAtIndexPath:top atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
}
var indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.sampleTableView.scrollToRowAtIndexPath(indexPath,
atScrollPosition: UITableViewScrollPosition.Top, animated: true)
or
self.sampleTableView.setContentOffset(CGPoint.zero, animated:false)
float systemVersion= [[[UIDevice currentDevice] systemVersion] floatValue];
if(systemVersion >= 7.0f)
{
self.edgesForExtendedLayout=UIRectEdgeNone;
}
Try this code in viewDidLoad() method.
Here is idStar's answer in updated Swift 3 syntax:
self.tableView.contentOffset = CGPoint(x: 0, y: 0 - self.tableView.contentInset.top)
With animation:
self.tableView.setContentOffset(CGPoint(x: 0, y: 0 - self.tableView.contentInset.top), animated: true)
Swift 3
If you have table view headers CGPointZero may not work for you, but this always does the trick to scroll to top.
self.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: UITableViewScrollPosition.top, animated: false)
you can still use scrollToRowAtIndexPath: for the purpose
I realize this has been answered but I just wanted to give another option:
CGRect frame = {{0, 0},{1, 1}};
[self.tableView scrollRectToVisible:frame animated:YES];
This always guarantees the UITableView will scroll to the top. The accepted answer:
NSIndexPath* top = [NSIndexPath indexPathForRow:NSNotFound inSection:0];
[self.tableView scrollToRowAtIndexPath:top atScrollPosition:UITableViewScrollPositionTop animated:YES];
was not working for me because I was scrolling to a tableHeaderView and not a cell.
Using scrollRectToVisible works on iOS 6 and 7.
Swift 4 UITableViewExtension:
func scrollToTop(animated: Bool) {
if numberOfSections > 0 {
let topIndexPath = IndexPath(row: NSNotFound, section: 0)
scrollToRow(at: topIndexPath, at: .top, animated: animated)
}
}
in swift I used:
self.tableView.setContentOffset(CGPointMake(0, 0), animated: true)
but #Alvin George's works great
I want to make next:
I have an UITableView that have to dispaly words (A-Z).
Currently when view did load I have one cell that displayed (and this is correct). First cell display first word from my array words.
Purpose:
I want to move to the cell that must display 10 word from my array, but problem is that the cell with indexPath.row = 10 does not exist (and this correct, because I don't scroll yet).
What is a right wait to make transition from 1 to 10 cell.
I think if I don't use dequeueReusableCellWithIdentifier for creating my cell I can do it and solve my problem, but I mean this problem for device memory.
In other words I need to make scrollToRowAtIndexPath
Thanks!
You are right to identify the scrollToRowAtIndexPath method. So all you need to do is create a fresh IndexPath with the row you wish to move to, e.g., row index = 10:
[tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:10 inSection:indexPath.section]
atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
//For iOS 7
[self.TableView reloadData];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:1 inSection:1];
[TableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
One can identify the row(in the eg. below it is forRow) that needs to be made visible and reload the row. Upon the completion of that one can scroll to that row.
let moveToIndexPath = NSIndexPath(forRow: forRow, inSection: 0)
self.tableView.reloadRowsAtIndexPaths([moveToIndexPath], withRowAnimation: .None)
self.tableView.scrollToRowAtIndexPath(moveToIndexPath, atScrollPosition: atScrollPosition, animated: true)
If you're trying to present a ÙITableViewController, calling tableView.reloadData() in viewDidLoad() or viewWillAppear() will cause noticeable UI lag before the presentation animation.
However, you will need to wait for the ÙITableView to have loaded its data and layed out its view, otherwise scrollToRow(at:, at:, animated:) will not work.
Here's the (slightly hacky) solution I came with up with for scrolling in a table view which is currently being presented:
private var needsToScroll = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if needsToScroll {
needsToScroll = false
DispatchQueue.main.async {
let indexPath = ...
tableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
needsToScroll is needed because viewDidLayoutSubviews() will often be called more than once, which is an issue since scrollToRow(at:, at:, animated:) can result in viewDidLayoutSubviews() being called again.