Prevent UITableView from scrolling when new cells are inserted - ios

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

Related

Scroll to bottom for Table View after Reload Data [duplicate]

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.

Animate View Off-Screen To The Right and Back In From the Left

I'm trying to replicate an animation found on the Apple App Store seen here. The app icons move from left to right continuously like a carousel and I'm trying to produce the same behaviour for a single UIView. Is anyone able to help with this? Really struggling:
Something I tried but it stops and repeats the animation without continuing from left to right.
UIView.animateKeyframes(withDuration: 5, delay: 0, options: .repeat, animations: {
viewOfImageViews.center.x += container.bounds.width +
}, completion: nil)
Here is one way you can archive this effect:
Add a collection view
Set cell width the same width as your superview
Set your collection view scroll direction to horizontal
Now in the code when the collection is loaded you scroll to the first section
DispatchQueue.main.async {
self.collView.scrollToItem(at: IndexPath(item: 0, section: 1), at: .left, animated: true)
}
Now you can add this code below so when animation is done it updates the cells and scroll again
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
colors.append(colors.removeFirst())
collView.reloadData()
DispatchQueue.main.async {
self.collView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .left, animated: false)
self.collView.scrollToItem(at: IndexPath(item: 0, section: 1), at: .left, animated: true)
}
}
Change colors array to your array name which contains the images

UITableView add rows and scroll to bottom

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/

UICollectionViewScrollPosition not working

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.

Scroll to top with estimated cell heights

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)

Resources