I'm writing a table view where rows are added upon user interaction. The general behavior is simply to add a single row, then scroll to the end of the table.
This was working perfectly fine before iOS11, but now the scrolling always jumps from the top of the table instead of smoothly scrolling.
Here's the code that has to do with adding new rows:
func updateLastRow() {
DispatchQueue.main.async {
let lastIndexPath = IndexPath(row: self.currentSteps.count - 1, section: 0)
self.tableView.beginUpdates()
self.tableView.insertRows(at: [lastIndexPath], with: .none)
self.adjustInsets()
self.tableView.endUpdates()
self.tableView.scrollToRow(at: lastIndexPath,
at: UITableViewScrollPosition.none,
animated: true)
}
}
And
func adjustInsets() {
let tableHeight = self.tableView.frame.height + 20
let table40pcHeight = tableHeight / 100 * 40
let bottomInset = tableHeight - table40pcHeight - self.loadedCells.last!.frame.height
let topInset = table40pcHeight
self.tableView.contentInset = UIEdgeInsetsMake(topInset, 0, bottomInset, 0)
}
I'm confident the error lies within the fact that multiple UI updates are pushed at the same time (adding row and recalculating edge insets), and tried chaining these functions with separate CATransaction objects, but that completely messes up asynchronous completion blocks defined elsewhere in the code which update some of the cell's UI elements.
So any help would be appreciated :)
I managed to fix the issue by simply just calling self.tableView.layoutIfNeeded() before adjusting the insets:
func updateLastRow() {
DispatchQueue.main.async {
let lastIndexPath = IndexPath(row: self.currentSteps.count - 1, section: 0)
self.tableView.beginUpdates()
self.tableView.insertRows(at: [lastIndexPath], with: .none)
self.tableView.endUpdates()
self.tableView.layoutIfNeeded()
self.adjustInsets()
self.tableView.scrollToRow(at: lastIndexPath,
at: UITableViewScrollPosition.bottom,
animated: true)
}
}
Make sure that you are respecting the Safe Areas.
For more details, check: https://developer.apple.com/ios/update-apps-for-iphone-x/
I used two collection views and they are connected to each other for scrolling. if one scroll the other one will scroll too.
this is handling in my didScroll delegate function as below:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var screenHeight = max(Int(UIScreen.mainScreen().bounds.width), Int(UIScreen.mainScreen().bounds.height))
if ( collectionView == self.bottomSliderCollectionView)
{
return CGSizeMake(self.sliderCollectionView.frame.width - 65, 50)
}else {
return CGSizeMake(self.sliderCollectionView.frame.width , self.sliderCollectionView.frame.height)
}
}
func scrollViewDidScroll(scrollView: UIScrollView) {
if (scrollView == self.sliderCollectionView ){
// Calculate where the collection view should be at the right-hand end item
var contentOffsetWhenFullyScrolledRight = self.sliderCollectionView.frame.size.width * CGFloat(self.workingMenuSlider.count - 1)
if (scrollView.contentOffset.x == contentOffsetWhenFullyScrolledRight ) {
// user is scrolling to the right from the last item to the 'fake' item 1.
// reposition offset to show the 'real' item 1 at the left-hand end of the collection view
var newIndexPath = NSIndexPath(forItem: 1, inSection: 0)
self.sliderCollectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
} else if (scrollView.contentOffset.x == 0) {
// user is scrolling to the left from the first item to the fake 'item N'.
// reposition offset to show the 'real' item N at the right end end of the collection view
var newIndexPath = NSIndexPath(forItem: self.workingMenuSlider.count - 2, inSection: 0)
self.sliderCollectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
}
}
if (scrollView.contentOffset.x < 0) {
var newIndexPath = NSIndexPath(forItem: self.workingMenuSlider.count - 2, inSection: 0)
self.sliderCollectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
self.bottomSliderCollectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
}
var contentOffsetWhenFullyScrolledRight = self.sliderCollectionView.frame.size.width * CGFloat(self.workingMenuSlider.count - 1)
if (scrollView.contentOffset.x > contentOffsetWhenFullyScrolledRight) {
var newIndexPath = NSIndexPath(forItem: 1, inSection: 0)
self.sliderCollectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
self.bottomSliderCollectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
}
if (self.sepratedScroll == true ) {
if (scrollView == self.sliderCollectionView ){
var nesbat = (self.bottomSliderCollectionView.frame.width - 65) / self.sliderCollectionView.frame.width
self.sepratedScroll = false
self.bottomSliderCollectionView.contentOffset.x = self.sliderCollectionView.contentOffset.x * nesbat
self.sepratedScroll = true
}else {
self.sepratedScroll = false
var nesbat = (self.bottomSliderCollectionView.frame.width - 65) / self.sliderCollectionView.frame.width
self.sliderCollectionView.contentOffset.x = (self.bottomSliderCollectionView.contentOffset.x ) / nesbat
self.sepratedScroll = true
}
} else {
}
}
as you see var nesbat is calculating a float which is because my bottomCollection View cell width is - 65 than the sliderCollectionView.
both of them set to paging scroll. the top one (SliderCollectionView) works well. and it is an infinite scrolling which has the duplicate value in my array of data.
so, my question here is when my bottomCollectionview slides, which is a paging scroll, it goes more than its cell width.
i want to scroll exactly the width of the cell in bottomCollectionView.
i tested the answers which are saying to handle it on didScroll View but it is ok for my case. I have too many if to handle the infinite scrolling and also the connected collection views.
please help me with the paging scroll to set on the cells width not the width of the whole collectionView.
thankx
okay i got it myself.
i change my top collection view height and move it over the bottom collection view. but the content in xib for collection view bottom constraints changed to + 50 .
so i have a collection view on top with its content is - 50 than the height of the collection view and it is over the bottomCollectionView.
so i have the suer interaction of my top one .
problem solved :D :)
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
In my table view I have to scroll to the top. But I cannot guarantee that the first object is going to be section 0, row 0. May be that my table view will start from section number 5.
So I get an exception, when I call:
[mainTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
Is there another way to scroll to the top of table view?
UITableView is a subclass of UIScrollView, so you can also use:
[mainTableView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES];
Or
[mainTableView setContentOffset:CGPointZero animated:YES];
And in Swift:
mainTableView.setContentOffset(CGPointZero, animated:true)
And in Swift 3 & above:
mainTableView.setContentOffset(.zero, animated: true)
Note: This answer isn't valid for iOS 11 and later.
I prefer
[mainTableView setContentOffset:CGPointZero animated:YES];
If you have a top inset on your table view, you have to subtract it:
[mainTableView setContentOffset:CGPointMake(0.0f, -mainTableView.contentInset.top) animated:YES];
Possible Actions:
1
func scrollToFirstRow() {
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Top, animated: true)
}
2
func scrollToLastRow() {
let indexPath = NSIndexPath(forRow: objects.count - 1, inSection: 0)
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
}
3
func scrollToSelectedRow() {
let selectedRows = self.tableView.indexPathsForSelectedRows
if let selectedRow = selectedRows?[0] as? NSIndexPath {
self.tableView.scrollToRowAtIndexPath(selectedRow, atScrollPosition: .Middle, animated: true)
}
}
4
func scrollToHeader() {
self.tableView.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: true)
}
5
func scrollToTop(){
self.tableView.setContentOffset(CGPointMake(0, UIApplication.sharedApplication().statusBarFrame.height ), animated: true)
}
Disable Scroll To Top:
func disableScrollsToTopPropertyOnAllSubviewsOf(view: UIView) {
for subview in view.subviews {
if let scrollView = subview as? UIScrollView {
(scrollView as UIScrollView).scrollsToTop = false
}
self.disableScrollsToTopPropertyOnAllSubviewsOf(subview as UIView)
}
}
Modify and use it as per requirement.
Swift 4
func scrollToFirstRow() {
let indexPath = IndexPath(row: 0, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
It's better to not use NSIndexPath (empty table), nor assume that top point is CGPointZero (content insets), that's what I use -
[tableView setContentOffset:CGPointMake(0.0f, -tableView.contentInset.top) animated:YES];
Hope this helps.
Swift 4:
This works very well:
//self.tableView.reloadData() if you want to use this line remember to put it before
let indexPath = IndexPath(row: 0, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
DONT USE
tableView.setContentOffset(.zero, animated: true)
It can sometimes set the offset improperly. For example, in my case, the cell was actually slightly above the view with safe area insets. Not good.
INSTEAD USE
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
On iOS 11, use adjustedContentInset to correctly scroll to top for both cases when the in-call status bar is visible or not.
if (#available(iOS 11.0, *)) {
[tableView setContentOffset:CGPointMake(0, -tableView.adjustedContentInset.top) animated:YES];
} else {
[tableView setContentOffset:CGPointMake(0, -tableView.contentInset.top) animated:YES];
}
Swift:
if #available(iOS 11.0, *) {
tableView.setContentOffset(CGPoint(x: 0, y: -tableView.adjustedContentInset.top), animated: true)
} else {
tableView.setContentOffset(CGPoint(x: 0, y: -tableView.contentInset.top), animated: true)
}
I've encountered an issue calling trying some of the methods on an empty tableView. Here's another option for Swift 4 that handles empty tableviews.
extension UITableView {
func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
return indexPath.section < self.numberOfSections && indexPath.row < self.numberOfRows(inSection: indexPath.section)
}
func scrollToTop(animated: Bool) {
let indexPath = IndexPath(row: 0, section: 0)
if self.hasRowAtIndexPath(indexPath: indexPath) {
self.scrollToRow(at: indexPath, at: .top, animated: animated)
}
}
}
Usage:
// from yourViewController or yourTableViewController
tableView.scrollToTop(animated: true)//or false
For tables that have a contentInset, setting the content offset to CGPointZero will not work. It'll scroll to the content top vs. scrolling to the table top.
Taking content inset into account produces this instead:
[tableView setContentOffset:CGPointMake(0, -tableView.contentInset.top) animated:NO];
This code let's you scroll a specific section to top
CGRect cellRect = [tableinstance rectForSection:section];
CGPoint origin = [tableinstacne convertPoint:cellRect.origin
fromView:<tableistance>];
[tableinstance setContentOffset:CGPointMake(0, origin.y)];
Swift 5, iOS 13
I know this question already has a lot of answers but from my experience this method always works:
let last = IndexPath(row: someArray.count - 1, section: 0)
tableView.scrollToRow(at: last, at: .bottom, animated: true)
And this is especially true if you're working with animations (like keyboard) or certain async tasks—the other answers will often scroll to the almost bottom. If for some reason this doesn't get you all the way to the bottom, it's almost certainly because of a competing animation so the workaround is to dispatch this animation to the end of the main queue:
DispatchQueue.main.async {
let last = IndexPath(row: self.someArray.count - 1, section: 0)
self.tableView.scrollToRow(at: last, at: .bottom, animated: true)
}
This may seem redundant since you're already on the main queue but it's not because it serializes the animations.
Swift:
tableView.setContentOffset(CGPointZero, animated: true)
Swift 3
tableView.setContentOffset(CGPoint.zero, animated: true)
if tableView.setContentOffset don't work.
Use:
tableView.beginUpdates()
tableView.setContentOffset(CGPoint.zero, animated: true)
tableView.endUpdates()
Since my tableView is full of all kinds of insets, this was the only thing that worked well:
Swift 3
if tableView.numberOfSections > 0 && tableView.numberOfRows(inSection: 0) > 0 {
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
}
Swift 2
if tableView.numberOfSections > 0 && tableView.numberOfRowsInSection(0) > 0 {
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: .Top, animated: true)
}
Adding on to what's already been said, you can create a extension (Swift) or category (Objective C) to make this easier in the future:
Swift:
extension UITableView {
func scrollToTop(animated: Bool) {
setContentOffset(CGPointZero, animated: animated)
}
}
Any time you want to scroll any given tableView to the top you can call the following code:
tableView.scrollToTop(animated: true)
I prefer the following, as it takes into account an inset. If there is no inset, it will still scroll to the top as the inset will be 0.
tableView.setContentOffset(CGPoint(x: 0, y: -tableView.contentInset.top), animated: true)
Swift :
if you don't have tableView header :
tableView.setContentOffset(CGPointMake(0, UIApplication.sharedApplication().statusBarFrame.height ), animated: true)
if so :
tableView.setContentOffset(CGPointMake(0, -tableViewheader.frame.height + UIApplication.sharedApplication().statusBarFrame.height ), animated: true)
In Swift 5 , Thanks #Adrian's answer a lot
extension UITableView{
func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
return indexPath.section < numberOfSections && indexPath.row < numberOfRows(inSection: indexPath.section)
}
func scrollToTop(_ animated: Bool = false) {
let indexPath = IndexPath(row: 0, section: 0)
if hasRowAtIndexPath(indexPath: indexPath) {
scrollToRow(at: indexPath, at: .top, animated: animated)
}
}
}
Usage:
tableView.scrollToTop()
Here's what I use to work correctly on iOS 11:
extension UIScrollView {
func scrollToTop(animated: Bool) {
var offset = contentOffset
if #available(iOS 11, *) {
offset.y = -adjustedContentInset.top
} else {
offset.y = -contentInset.top
}
setContentOffset(offset, animated: animated)
}
}
using contentOffset is not the right way. this would be better as it is table view's natural way
tableView.scrollToRow(at: NSIndexPath.init(row: 0, section: 0) as IndexPath, at: .top, animated: true)
This was the only code snippet that worked for me
Swift 4:
tableView.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: true)
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
tableView.setContentOffset(CGPoint(x: 0, y: -70), animated: true)
P.S. 70 is the height of my header and table view cell
func scrollToTop() {
NSIndexPath *topItem = [NSIndexPath indexPathForItem:0 inSection:0];
[tableView scrollToRowAtIndexPath:topItem atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
call this function wherever you want UITableView scroll to top
Swift 4 via extension, handles empty table view:
extension UITableView {
func scrollToTop(animated: Bool) {
self.setContentOffset(CGPoint.zero, animated: animated);
}
}
I use tabBarController and i have a few section in my tableview at every tab, so this is best solution for me.
extension UITableView {
func scrollToTop(){
for index in 0...numberOfSections - 1 {
if numberOfSections > 0 && numberOfRows(inSection: index) > 0 {
scrollToRow(at: IndexPath(row: 0, section: index), at: .top, animated: true)
break
}
if index == numberOfSections - 1 {
setContentOffset(.zero, animated: true)
break
}
}
}
}
I had to add the multiply by -1 * to the sum of the status bar and the navigation bar, because it was going that height off the screen,
self.tableView.setContentOffset(CGPointMake(0 , -1 *
(self.navigationController!.navigationBar.height +
UIApplication.sharedApplication().statusBarFrame.height) ), animated:true)
in swift
your row = selectioncellRowNumber
your section if you have = selectionNumber if you dont have set is to zero
//UITableViewScrollPosition.Middle or Bottom or Top
var lastIndex = NSIndexPath(forRow: selectioncellRowNumber, inSection: selectionNumber)
self.tableView.scrollToRowAtIndexPath(lastIndex, atScrollPosition: UITableViewScrollPosition.Middle, animated: true)
Here Is The Code To ScrollTableView To Top Programatically
Swift:
self.TableView.setContentOffset(CGPointMake(0, 1), animated:true)
In Swift-3 :
self.tableView.setContentOffset(CGPoint.zero, animated: true)
If you need use Objective-C or you still in love:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[_tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
Solution with scrollToRow which fixes the problem for empty TableView (needs for search).
import UIKit
extension UITableView {
public func scrollToTop(animated: Bool = false) {
if numberOfRows(inSection: 0) > 0 {
scrollToRow(
at: .init(row: 0, section: 0),
at: .top,
animated: animated
)
}
}
}