This question already has answers here:
How can I tell when UITableView has completed ReloadData?
(18 answers)
Closed 2 years ago.
I am trying to scroll my table view to bottom after adding comments and replies. Comments are sections and replies are rows for that particular section but table view doesn't scroll at the exact bottom of the content after API call for adding comment, I have tried every possible solution available on net. Kindly suggest any solution for the same
This can be done with simple TableView scroll to bottom using IndexPath
tblView.scrollToRow(at: IndexPath(row: 8, section: 0), at: .bottom, animated: true)
From Jayraj Vala answer
You've calculate the contentSize of the tableView to scroll at any particular point Use below code to scroll to bottom of tableView.
func scrollToBottom() {
let point = CGPoint(x: 0, y: self.tableView.contentSize.height + self.tableView.contentInset.bottom - self.tableView.frame.height)
if point.y >= 0{
self.tableView.setContentOffset(point, animated: animate)
}
}
Use described func() when you want to scroll tableView.
As an extension for UITableView:
extension UITableView {
func scrollToBottom(withAnimation animated: Bool = true) {
let rowCount = self.numberOfRows(inSection: self.numberOfSections - 1) - 1
// This ensures we don't scroll to the bottom if there is no content
guard rowCount > 0 else { return }
let point = CGPoint(x: 0, y: self.contentSize.height + self.contentInset.bottom - self.bounds.height)
// This ensures we don't scroll to the bottom if all the content is small enough to be displayed without a scroll
guard point.y >= 0 else { return }
self.setContentOffset(point, animated: animated)
}
}
Be sure to call this after tableView.reloadData() otherwise the table view contentSize used will be incorrect.
Related
I've seen this question being asked several times and, despite having implemented each proposed solution by the community, I still haven't succeeded. What I'm implementing is a basic public chat app. I need to display many messages that I receive through my API inside a UITableView. In order to have a chat feeling, I've turned both my UITableView and UITableViewCell upside down by changing their transform property to CGAffineTransform(scaleX: 1, y: -1). Instead, to add cells to the UITableView, I first add the incoming message to the array via messages.insert(message, at: indexPath.row)and then I call insertRows(at: [indexPath], with: animation) (where indexPath is created this way IndexPath(row: 0, section: 0)). When I'm at the bottom of the UITableView everything works great: the new cells appear from bottom to top accompanied by a smooth animation. The problems start when I scroll to top by a few pixels. Take a look at these images to better understand the difference.
What I would like to achieve is preventing the UITableView from scrolling unless I'm at its very bottom so that, if the user scrolls to top with the aim of reading a past message, he can do so without any trouble caused by the movement of the UITableView.
I hope someone can point me in the right direction. Thanks
Edit: I'm using automatic UITableViewCell height if that helps.
Edit: here's my current code:
I'm using a generic wrapper class ListView<Cell: UITableViewCell, Item> with this method used for adding new items:
func add(_ item: Item) {
items.insert(item, at: 0)
if contentOffset.y > -contentInset.top {
insertRows(at: [IndexPath(row: 0, section: 0)], with: .top)
} else {
reloadData()
}
}
I had to use -contentInset.top to check if I'm at the very bottom of the scroll view since I've previously set the contentInset to UIEdgeInsets(top: composeMessageView.frame.height - 4, left: 0, bottom: 4, right: 0) for layout reasons. Again, I've set estimatedRowHeight to 44 and rowHeight to UITableViewAutomaticDimension.
func add(_ item: Item) {
// Calculate your `contentOffset` before adding new row
let additionalHeight = tableView.contentSize.height - tableView.frame.size.height
let yOffset = tableView.contentOffset.y
// Update your contentInset to start tableView from bottom of page
updateTableContentInset()
items.append(item)
// Create indexPath and add new row at the end
let indexPath = IndexPath(row: objects.count - 1, section: 0)
tableView.insertRows(at: [indexPath], with: .top)
// Scroll to new added row if you are viewing latest messages otherwise stay at where you are
if yOffset >= additionalHeight {
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
Here is the method to update contentInset. It will give you the same effect which you were achieving by this CGAffineTransform(scaleX: 1, y: -1)
func updateTableContentInset() {
var contentInsetTop = tableView.frame.size.height - tableView.contentSize.height
if contentInsetTop <= 0 {
contentInsetTop = 0
}
tableView.contentInset = UIEdgeInsets(top: contentInsetTop, left: 0, bottom: 0, right: 0)
}
I created a horizontal tableView, with cells that take up the whole view controller. Instead of scrolling with the default setting, I would like to scroll to the next cell using scrollView.scrollToItem(at: IndexPath method.
Meaning, each time the user scrolls right, it will automatically scroll to the next cell, and each time the user swipes left, it will automatically scroll to the previous cell. Whenever you scroll a direction, it should automatically scroll to the next or previous cell.
I tried intercepting it using scrollViewDidScroll delegate method, but I am running into a ton of problems, it is autoscrolling back and forth and glitching a ton. What am I doing wrong?
var previousOffset: CGFloat = 0.0
var allowHorizontalScroll: Bool = true
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (allowHorizontalScroll){
let offset = scrollView.contentOffset.x
let diff = previousOffset - offset
previousOffset = offset
var currentBoardIndex = scrollView.indexPathForItem(at: CGPoint(x:offset, y:10))?.item
if currentBoardIndex != nil {
if diff > 0 {
//print("scroll left")
if (currentBoardIndex != 0){
currentBoardIndex = currentBoardIndex! - 1
allowHorizontalScroll = false
scrollView.scrollToItem(at: IndexPath(row: (currentBoardIndex!), section: 0), at: .left, animated: true)
}
}else{
//print("scroll right")
if (currentBoardIndex != ((boardVCDataSource?.fetchedResultsController.fetchedObjects?.count)! - 1)){
currentBoardIndex = currentBoardIndex! + 1
allowHorizontalScroll = false
scrollView.scrollToItem(at: IndexPath(row: (currentBoardIndex!), section: 0), at: .left, animated: true)
}
}
}
}
}
Whenever you scroll a direction, it should automatically scroll to the next or previous cell.
Why not throw out all that code, and instead set your scroll view's isPagingEnabled to true? A paging scroll view does what you describe, automatically.
I have a UITableView with about 500 items.
When i call tableView.scrollToRow(at: indexPath, at: .bottom, animated: false) the main UI thread is getting blocked for 3 seconds.
Is there a way to fix this? or is the problem scrolling 500 items?
Thanks
the problem is not with reloadData it was with scrollToRow
From discussion about how to use a table view for chat:
We can use a table view which uses a transform to flip the Y coordinate. We then need to do the same for each of the cells so they are not upside down.
The procedure is to build a normal messaging table view where the newest message is on top (instead of bottom). Then put the table view on some superview and invert its coordinate system:
chatContainer?.transform = CGAffineTransform(scaleX: 1.0, y: -1.0)
The cell containing the messages should also have some sort of superview for all the contents which needs to be flipped:
override func awakeFromNib() {
super.awakeFromNib()
containerView?.transform = CGAffineTransform(scaleX: 1.0, y: -1.0)
}
So the cell is basically flipped twice so it is shown correctly.
You may find an example project here.
Use time profiler to identify where exactly is the issue. The thing is that in general the UITableView performance is not effected by a number of items loaded. The view itself will load as many items as it needs to fill the whole screen.
You may test this by logging a method in a cellForRowAtIndexPath. So I am guessing this method may be the one that is slow. Check how you access the data from it, maybe there is some heavy logic on it. Or the cell layout may be bugged and very slow.
In a general case if you have extremely large amount of data consider using core data and NSFetchedResultsController which is designed specifically for this situations. But still note that loading 500 elements in a table view should work smoothly without any special optimizations.
You should do something like this; If user scroll down from top to bottom of the tableview scrollview delegate method fire its "scrollViewDidScroll" method and detect if user bottom of tableview or not then fetch other data and append your array and reload the tableview. Thats it!
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat actualPosition = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height - (someArbitraryNumber);
if (actualPosition >= contentHeight) {
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
}
I'm not 100% sure about this solution, didn't had that problem myself.
Maybe just dispatch it?
extension UITableView {
func tableViewScrollToBottom(animated: Bool) {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
let numberOfSections = self.numberOfSections
let numberOfRows = self.numberOfRows(inSection: numberOfSections-1)
if numberOfRows > 0 {
let indexPath = IndexPath(row: numberOfRows-1, section: (numberOfSections-1))
self.scrollToRow(at: indexPath, at: UITableViewScrollPosition.bottom, animated: animated)
}
}
}
}
i have a collectionview that i am trying to scroll programatically. the problem being that if the cell i want to scroll to is visible in the collection view it doesn't scroll it to the centre. so for the image below the lower cell is item 1. and it does not scroll to it but it will scroll past item 1 to item 2.
i have been trying to use UICollectionVieScrollPosition.CenterVertically but this does not seem to work.
self.collectionView?.scrollToItem(at: IndexPath(row: 1, section: 0), at: UICollectionViewScrollPosition.centeredVertically, animated: true)
is there a way around this to force the scrolling of cells that are visible to the centre of the collection?
the best way i found to do this is to not use scrollToItem but to get the CGRect of the index and then make that visible.
let rect = self.collectionView.layoutAttributesForItem(at: IndexPath(row: 5, section: 0))?.frame
self.collectionView.scrollRectToVisible(rect!, animated: false)
I'm trying to delay it with 0.1s. For my case, looks good for now:
collectionView.collectionViewLayout.invalidateLayout() //just in case, iOS10 may crash btw.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
Update:
ok, turns out that I'm using layout.estimatedItemSize and autolayout to calculate the width of my cells, that's why I have this problem.
That says, for me, it's because of CollectionView's dynamic sizing.
After I back to calculate the width manually, everything works fine. (by using -collectionView:layout:sizeForItemAt:)
First of all you need to find the center of the CollectionView and this is how it can be done:
private func findCenterIndex() -> Int {
let center = self.view.convert(numberCollectionView.center, to: self.numberCollectionView)
let row = numberCollectionView!.indexPathForItem(at: center)?.row
guard let index = row else {
return 0
}
return index
}
then get the row number under your scrollToItem and it should work:
collectionView.scrollToItem(at: IndexPath(item: findCenterIndex(), section: 0), at: .centeredVertically, animated: false)
call it from viewDidLayoutSubviews()
If itemSize is too small,scrollToItemnot working.
siwft
collectionView.contentOffset = offset
I use this fixed
It seems that you have centred the first cell in your UICollectionView vertically.
I have found that if I centred the first cell by adding an inset through the contentInset property of UICollectionView, its scrollToItem(at:at:animated:) method also doesn't work for cells already visible.
However, if I centred the first cell by adding an inset through the sectionInset property of UICollectionViewFlowLayout, then scrollToItem(at:at:animated:) works for visible cells.
Specifically, in code:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Padding needed to allow the first and last cells to be centred vertically.
let insectHeight = (collectionView.bounds.height - collectionViewFlowLayout.itemSize.height) / 2.0
// This way is disabled because scrollToItem doesn't work for visible cells.
// collectionView.contentInset = UIEdgeInsets(top: insectHeight,
// left: 0,
// bottom: insectHeight,
// right: 0)
// This is the way for scrollToItem to work for visible cells.
collectionViewFlowLayout.sectionInset = UIEdgeInsets(top: insectHeight,
left: 0,
bottom: insectHeight,
right: 0)
}
My guess is that contentInset is a property UICollectionView inherited from UIScrollView, and the way scrollToItem(at:at:animated:) works out the offset seems incompatible to the way contentInset is used by UIScrollView.
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)