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/
Related
I currently have a login and signup buttons labeled as 1 and 2. And underneath that, I have a collectionView with two cells (one for the login UI and one for register UI). When I click on the signup button(Label 2), I want to move the collectionView to the next cell.
I tried using the scrollToItem, but it does not do anything. Anyone know why?
#objc private func didTapRegisterButton() {
let i = IndexPath(item: 1, section: 0)
self.onboardingCollectionView.scrollToItem(at: i, at: .right, animated: true)
}
Figured it out. My collectionView had isPagingEnabled property as true. The scrollToItem doesn't work if that is set to true.
If you want to scroll between two cells on button click then first you need to uncheck(disable) Scrolling Enabled property and check(enable) Paging Enabled property of collectionView. (With this approach user can't scroll manually with swipe)
On button first action,
if self.currentIndexPath == 0 {
return
} else {
self.currentIndexPath += 1
self.yourCollectionView.scrollToItem(at: IndexPath(row: self.currentVisibleIndexPath, section: 0), at: .centeredHorizontally, animated: true)
}
On button second action,
if self.currentIndexPath == 1 {
self.currentIndexPath -= 1
self.yourCollectionView.scrollToItem(at: IndexPath(row: self.currentVisibleIndexPath, section: 0), at: .centeredHorizontally, animated: true)
} else {
return
}
And declare global variable
var currentIndexPath: Int = 0
I'm using a collection view, UICollectionView, and it works absolutely great...except that I cannot scroll to any particular item. It seems to always scroll to the "middle" is my best guess. In any case, whatever I send to scrollToItem seems to have no effect on the scrolling. I've put it in various locations throughout my view controller but with no success.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let lastIndex = IndexPath(row: messages.count-1, section: 0)
self.messagesView.scrollToItem(at: lastIndex, at: .bottom, animated: true)
}
UICollection View has bug in iOS 14 with scrollToItem. In iOS 14 it will only work if collection view paging will be desable.
So i got a solution if we have to scroll with both manual and programatically.
Specially for iOS 14 and above
self.collView.isPagingEnabled = false
self.collView.scrollToItem(at: IndexPath(item: scrollingIndex, section: 0), at: .left, animated: true)
self.collView.isPagingEnabled = true
You could try putting the scrolling code in viewDidLayoutSubviews which should get called after all table cells are loaded, which means your messages.count should work. Also, make sure you only have a single section in your collection view.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.scrollToBottom()
}
func scrollToBottom() {
DispatchQueue.main.async {
let lastIndex = IndexPath(item: messages.count-1, section: 0)
self.messagesView.scrollToItem(at: lastIndex, at: .bottom, animated: true)
}
}
Swift 5 / iOS 15
I was facing the same problem, so I tried this one-line code and get it done.
// here we slect the required cell instead of directly scrolling to the item and both work same.
self.collectionView.selectItem(at: IndexPath(row: self.index, section: 0), animated: true, scrollPosition: .left)
This is what worked for me, in case you want paging enabled:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.isPagingEnabled = false
collectionView.scrollToItem(
at: IndexPath(item: 1, section: 0),
at: .centeredVertically,
animated: false
)
collectionView.isPagingEnabled = true
}
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 need to scroll to bottom. I have a chat app like whatsapp. So when view appears table view should show last row. I am achiving this with following line and works nice.
tableView.setContentOffset(CGPointMake(0, CGFloat.max), animated: false)
Also I need to scroll to bottom when keyboard appears. I am using Auto Layout and above line is not working. For to do this i am using following line:
func scrollToLastRow(animated: Bool) {
if self.numberOfRowsInSection(0) > 0 {
self.scrollToRowAtIndexPath(NSIndexPath(forRow: self.numberOfRowsInSection(0) - 1, inSection: 0), atScrollPosition: .Bottom, animated: animated)
}
}
This is a extension for Tableview.
This solution is working fine when there is no too much message. Then I tried with 5000 messages (so tableview have 5000 rows, but i am paging them) And when keyboard appears i see cpu usage is %98-100. I think the second code is problem for pagination, it causes loading every message to ram and my app freezes and receiving ram warning.
How to scroll to bottom without any performance issue?
If you have pagination, you can try to only load your current page as well as the final page, assuming you have 20 messages in each page, in this case your table have 40 rows only. Then you can use your function:
func scrollToLastRow(animated: Bool) {
if self.numberOfRowsInSection(0) > 0 {
self.scrollToRowAtIndexPath(NSIndexPath(forRow: self.numberOfRowsInSection(0) - 1, inSection: 0), atScrollPosition: .Bottom, animated: animated)
}
}
try this method :
let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
let numberOfSections = self.tableView.numberOfSections
let numberOfRows = self.tableView.numberOfRowsInSection(numberOfSections-1)
if numberOfRows > 0 {
let indexPath = NSIndexPath(forRow: numberOfRows-1, inSection: (numberOfSections-1))
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Bottom, animated: true)
}
})
If your are creating IM app. I recommend you reverse the tableView.
So the first row appears at bottom, and don't needs scrolling at beginning anymore.
Here is a cocoapod could help: https://github.com/marty-suzuki/ReverseExtension
If you stil want to scroll to specific row
implement the UIScrollViewDelegate.scrollViewDidEndScrollingAnimation.
and goes like this.
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if scrollView.contentSize.height > lastContentHeight {
self.tableView.scrollToBottom()
self.lastContentHeight = scrollView.contentSize.height
}
}
I have a UITableView with cells that are dynamically updated. Everything works fine apart from when tableview.reload is called (see below) to refresh the cells in the table I would like the table to scroll to the bottom to show the new entries.
- (void)reloadTable:(NSNotification *)notification {
NSLog(#"RELOAD TABLE ...");
[customTableView reloadData];
// Scroll to bottom of UITable here ....
}
I was planning to use scrollToRowAtIndexPath:atScrollPosition:animated: but then noticed that I don't have access to an indexPath.
Does anyone know how to do this, or of a delegate callback that I could use?
Use:
NSIndexPath* ipath = [NSIndexPath indexPathForRow: cells_count-1 inSection: sections_count-1];
[tableView scrollToRowAtIndexPath: ipath atScrollPosition: UITableViewScrollPositionTop animated: YES];
Or you can specify the section index manually (If one section => index=0).
Another solution is to flip the table vertically, and flip each cell vertically:
Apply the transform to the UITableView when initializing:
tableview.transform = CGAffineTransformMakeScale(1, -1);
and in cellForRowAtIndexPath:
cell.transform = CGAffineTransformMakeScale(1, -1);
This way you don't need workarounds for scrolling issues, but you will need to think a little harder about contentInsets/contentOffsets and header/footer interactions.
-(void)scrollToBottom{
[self.tableView scrollRectToVisible:CGRectMake(0, self.tableView.contentSize.height - self.tableView.bounds.size.height, self.tableView.bounds.size.width, self.tableView.bounds.size.height) animated:YES];
}
//In swift
var iPath = NSIndexPath(forRow: self.tableView.numberOfRowsInSection(0)-1,
inSection: self.tableView.numberOfSections()-1)
self.tableView.scrollToRowAtIndexPath(iPath,
atScrollPosition: UITableViewScrollPosition.Bottom,
animated: true)
Swift 3
For all the folks here trying to figure out how to solve this problem the key is to call the .layoutIfNeeded() method after .reloadData() :
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.setContentOffset(CGPoint(x: 0, y: tableView.contentSize.height - tableView.frame.height), animated: false)
I was working with multiple sections in UITableView and it worked well.
Fot Swift 5
extension UITableView {
func scrollToBottom(animated: Bool = true) {
let section = self.numberOfSections
if section > 0 {
let row = self.numberOfRows(inSection: section - 1)
if row > 0 {
self.scrollToRow(at: IndexPath(row: row-1, section: section-1), at: .bottom, animated: animated)
}
}
}
}
As this is something you might want to use really often, I suggest that you create a class extension on UITableView :
extension UITableView {
func scrollToBottom(animated: Bool = true) {
let section = self.numberOfSections
if section > 0 {
let row = self.numberOfRowsInSection(section - 1)
if row > 0 {
self.scrollToRowAtIndexPath(NSIndexPath(forRow: row - 1, inSection: section - 1), atScrollPosition: .Bottom, animated: animated)
}
}
}
}
This is another solution, worked well in my case, when cell height is big.
- (void)scrollToBottom
{
CGPoint bottomOffset = CGPointMake(0, _bubbleTable.contentSize.height - _bubbleTable.bounds.size.height);
if ( bottomOffset.y > 0 ) {
[_bubbleTable setContentOffset:bottomOffset animated:YES];
}
}
extension is better to be done on UIScrollView instead of UITableView, this way it works on scrollView, tableView, collectionView (vertical), UIWebView (inner scroll view), etc
public extension UIScrollView {
public func scrollToBottom(animated animated: Bool) {
let rect = CGRectMake(0, contentSize.height - bounds.size.height, bounds.size.width, bounds.size.height)
scrollRectToVisible(rect, animated: animated)
}
}
Swift 5
func scrollToBottom() {
let section = self.tableView.numberOfSections
let row = self.tableView.numberOfRows(inSection: self.tableView.numberOfSections - 1) - 1;
guard (section > 0) && (row > 0) else{ // check bounds
return
}
let indexPath = IndexPath(row: row-1, section: section-1)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
I dont agree that we should user cells_count,sections_count,self.dateSource.countand so on, Instead, the delegate will be better.
This is the best way.
- (void)scrollToBottom
{
CGFloat yOffset = 0;
if (self.tableView.contentSize.height > self.tableView.bounds.size.height) {
yOffset = self.tableView.contentSize.height - self.tableView.bounds.size.height;
}
[self.tableView setContentOffset:CGPointMake(0, yOffset) animated:NO];
}
try this code, It may help you:
self.tableView.reloadData()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+0.1, execute: {
let indexPath = IndexPath(row: self.dateSource.count-1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.bottom, animated: true)
})