UITableView ScrollToRow function animating weirdly - ios

The ScrollToRow is not working as I want.
The function I wrote seems to be working fine, but I want it to scroll down to last row only when its added like it happens in chat. When you get a new message, your tableview scrolls to the bottom to read new message.
However, my tableview seems to reload every time before performing scrollToLastRow function (what I mean is every time before scrolling to bottom... it first goes back to top first cell and starts scrolling to bottom from there Every time when a new cell is created).
My code is simple for the scroll:
func scrollToLastRow()
{
let indexPath = IndexPath(row: messages.count - 1, section: 0)
self.chat.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
To check my code:
here's a link to my repository
ChatApplication
the ViewController's used for chat is ---> chatVC
the ViewController used to make chat is -> chatCell
I have tried anything I could get all over I could find.
Tried dispatch (without it the scroll function wasn't working at all)
Used the function on many different places (including viewDidAppear)
Used dispatch.asyncAfter for delay.....
but nothing worked.

#Fahad, you are reloading the tableview before the scrollToLast function is getting called. Please check and please verify at your end.
Note please update your GitHub repository url in your question properly as it not clickable.
Use this function for scrolling to the bottom.
func scrollToBottom() {
let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height + contentInset.bottom)
setContentOffset(bottomOffset, animated: true)
}
use tableview.scrollToBottom()

Use tableView.insertRows(at: [IndexPath], with: UITableView.RowAnimation) for adding object to end of the tableView.
From the problem you say and looking in to your code, the animation issues are due to reloading the entire tableView after a new item is added to dataSource array,
What you have to do is to use Call tableView.insertRows(at: [IndexPath], with: UITableView.RowAnimation) with the correct indexPath on your tableView and the tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) will be invoked.

Related

Loading N views of UICollectionView and reload at the end

[EDIT after replies: solution at the end of the question]
I have a UICollectionView where I initially load 4 views. I would like that when the user scrolls until the last one (4th view) and try to scroll again, I update the data of my model, reload the UICollectionView and moves back to the 1st view so that the user can again scroll 3 times till the 4th view, etc.
The issue is that when I load 4 views initially, the user can't scroll after he reached the last view since there is nothing after. So I created an empty cell as my 5th view and now I load 5 views in my initial UICollectionView. But this 5th view is never fully displayed and I use "willDisplay" method to reload the data and force the move back to the first view. This is not elegant but I couldn't find another way to do it.
This method is working fine but here is the problem: it seems that when willDisplay is called, I indeed reload the data and moves back to the first view but only then the scroll that was supposed to take me to the 5th view is taken into account which means that after I moved back to the first view, the app shows me actually the second view (it did move back to the first view but then the scroll that I initiated to go from 4th to 5th view is applied, moving me to the 2nd view).
I hope this is clear...
So here is the code I am using:
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// if this is the last view
if((indexPath.row == self.dataModel.getNumberOfData()) && self.dataModel.getNumberOfData() > 0)
{
// I update the model here
self.dataModel.resetData()
// Then I reload the UICollectionView
self.collectionView.reloadData()
// Then I move back to the first view
self.collectionView.selectItem(at: IndexPath(item: 0, section: 0), animated: false, scrollPosition: .centeredHorizontally)
}
}
Of course if you have another solution, more elegant, to avoid that 5th view, I am happy to consider it. My data is not static so when I need to reload the new data, I query a database to retrieve the new information so I don't know about the new data when I run the app.
Thanks.
[EDIT with a solution using scrollViewDidScroll. I show for example how to detect the end (horizontally scrolling and delete the first view...
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
// if reached the end of the UICollectionView...
if (scrollView.contentOffset.x == scrollView.contentSize.width - scrollView.frame.size.width) {
print("Reached the end...")
self.dataModel.removeNFirst(n: 1) // remove first item from model
// delete the first view of the UICollectionView
self.collectionView.performBatchUpdates({
self.collectionView.deleteItems(at: [IndexPath(item: 0, section: 0)])
}) { (finished) in
self.collectionView.reloadData()
}
}
}

Why does Collection View load center carousel with this code?

I've been trying to figure out how to make my collection view of 3 cells load with the 2nd cell, and I finally figured it out after looking through StackOverFlow. However, the code that I came across is a bit confusing to me. Would anyone be able to explain why this code below works in making my collection view cell (that covers the whole screen) start with the 2nd of 3 cells? (this is the effect I wanted to achieve all along, but I want to learn more about why this code works exactly.
In this block of code, there's a bool variable and an if statement, why are they needed? When I took out the boolean variable and if statement, the collection view was unable to scroll.
How does this block of code work exactly?
Thank you.
var onceOnly = false
internal override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if !onceOnly {
let indexToScrollTo = IndexPath(item: 1, section: 0)
self.collectionView.scrollToItem(at: indexToScrollTo, at: .left, animated: false)
onceOnly = true
}
}
1- This scrollToItem in Docs
let indexToScrollTo = IndexPath(item: 1, section: 0)
self.collectionView.scrollToItem(at: indexToScrollTo, at: .left, animated: false)
makes the collectionView scroll to the second item in the list
2-
When I took out the boolean variable and if statement, the collection view was unable to scroll
Because willDisplay is called for every cell display , so when for example you scroll to 3rd cell willdisplay is called and causes the collectionView to go to the second cell , so it makes it stuck in the second item all the time ( and this seems like no scroll but the scroll happens and you won't notice that as it happens instantly ) , so the boolean var is needed to make that scroll action happens once which is the scroll to the specified index

UITableView is jumping when I insert new rows

I tried many ways to solve this problem, but I couldn't. My tableView jumps after it loads more data. I call the downloading method in willDisplay:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastObject = objects.count - 1
if indexPath.row == lastObject {
page = page + 1
getObjects(page: page)
}
}
and insert rows here:
func getObjects(page: Int) {
RequestManager.sharedInstance.getObjects(page: page, success: { objects in
DispatchQueue.main.async(execute: {
self.objects = self.objects + objects
self.tableView.beginUpdates()
var indexPaths = [IndexPath]()
for i in 0...objects.count - 1 {
indexPaths.append(IndexPath(row: i, section: 0))
}
self.tableView.insertRows(at: indexPaths, with: .bottom)
self.tableView.endUpdates()
});
})
}
So what do I wrong? Why tableView jumps after inserting new rows?
I have just find the solution to stop jumping the table view while
inserting multiple rows in the table View. Am facing this issue from
last few days so finally I resolved the issue.
We have to just set the content offset of table view while
inserting rows in the table view. You have to just pass your array of
IndexPath rest you can use this function.
Hope so this method will help you.
func insertRows() {
if #available(iOS 11.0, *) {
self.tableView.performBatchUpdates({
self.tableView.setContentOffset(self.tableView.contentOffset, animated: false)
self.tableView.insertRows(at: [IndexPath], with: .bottom)
}, completion: nil)
} else {
// Fallback on earlier versions
self.tableView.beginUpdates()
self.tableView.setContentOffset(self.tableView.contentOffset, animated: false)
self.tableView.insertRows(at: [IndexPath], with: .right)
self.tableView.endUpdates()
}
}
I had a similar problem with tableView. Partially I decided this with beginUpdates() and endUpdates()
self.tableView.beginUpdates()
self.tableView.endUpdates()
But this didn't solve the problem.
For iOS 11, the problem remained.
I added an array with the heights of all the cells and used this data in the method tableView(_:heightForRowAt:)
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row] ?? 0
}
Also add this method tableView(_:estimatedHeightForRowAt:)
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row] ?? 0
}
After that, the jumps stopped.
First, check your tableView(_:estimatedHeightForRowAt:) - this will never be accurate but the more likely the cell height ends up with this estimate the less work the table view will do.
So if there are 100 cells in your table view, 50 of them you are sure will end up with a height of 75 - that should be the estimate.
Also it's worth a while noting that there is no limit on the number of times the table view may ask its delegate of the exact cell height. So if you have a table view of 1000 rows there will a big performance issue on the layout out of the cells (delays in seconds) - implementing the estimate reduces drastically these calls.
Second thing you need to revisit the cell design, are there any views or controls whose height need to calculated by the table view? Like an image with top and bottom anchors equivalent to some other view whose height changes from cell to cell?
The more fixed heights these views/ controls have the easier it becomes for the table view to layout its cells.
I had the same issue with two table views, one of them had a variable height image embedded into a stack view where I had to implement the estimate. The other didn't had fixed size images and I didn't need to implement the estimate to make it scroll smoothly.
Both table views use pagination.
Last but not least, arrays are structs. structs are value types. So maybe you don't want to store any heights in an array, see how many copies you're making?
calculating the heights inside tableView(_:heightForRowAt:) is quite fast and efficient enough to work out really well.
Because your loop runs from 0 to objects count:
for i in 0...objects.count - 1 {
indexPaths.append(IndexPath(row: i, section: 0))
}
The indexpaths generated counting for row 0 till object's count. and hence the rows are getting added at top of table (i.e. at row 0) and hence causing tableview to jump as you are there at bottom of tableview.
Try changing range as:
let rangeStart = self.objects.count
let rangeEnd = rangeStart + (objects.count - 1)
for i in rangeStart...rangeEnd {
indexPaths.append(IndexPath(row: i, section: 0))
}
Hope it helps..!!!

UITableView dynamic cell height scroll to top after calling reloadData

I have dynamic cell height in my tableView
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100
I want to do same as whatsapp app (pagination), i want to load 25 by 25 row so when user scroll at top, i want to load new data but i want to keep same position of scroll, here is what i did
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if isMore && scrollView.contentOffset.y == 0 {
loadMore() // function that add new data in my model
self.tableView.reloadData()
}
}
The problem that i have is after i reach top of my tableview it call reloadData but the tableView scroll to UP
I tester some other solution like this:
var cellHeights: [IndexPath : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let height = cellHeights[indexPath] else { return 70.0 }
return height
}
But still not working
If you are reloading the table to add more data , then after reload call below function to retain the scroll position.
tableview.scrollToRow(at: indexPath, at: .middle, animated: true)
here indexPath is the indexPath of the last cell before reload.
In tableView:cellForRowAt when you detect indexPath is getting close to the last element (when indexPath.row is getting close to yourArray.count), query for more items. When you have them, add them to your data source (the array, or whatever it is, where you have all the objects represented in the table) and reload the table view. The table view should display the new items and the user can continue scrolling without doing anything special.
You shouldn't wait until tableView:cellForRowAt is called for the last cell, as the user will have to wait if you need to call a server to fetch more items. Whatsapp's scroll is fast because they download the items well before you scroll to the last elements (some time ago it did stop at the end, but now they are probably fetching new items way earlier).
Keep in mind that 25 items might be too few, it the network is slow the user might scroll through the items you have before more items to display and will have to wait, which is not a good user experience. Simulate a slow network connection and see if 25 items are enough to scroll through while your app contacts the server (depends on network speed, request and response sizes, how fast the server answers your request, etc.).
I faced the same problem in a messaging App. Here's how I solved it:
Assigned an unique messageId to every message in datasource array.
In cellForRow:, I save that messageId of very first row in a variable called messageIdFirst.
if(indexPath.row == 0){
messageIdFirst = message.id!
}
In loadMore() method, I check which current indexpath row have the same messageId in now updated datasource array.
if(message.id == messageIdFirst){
previousTopIndexPath = i
}
,where i being that index in datasource array which have the same messageId.
Now, I scroll to that previousTopIndexPath like this:
tableview.scrollToRow(at: previousTopIndexPath, at: .middle, animated: true)
Note 1: As this was not working efficiently in UITableView, I end up using UICollectionView with the same solution.
Note 2: While this does work smoothly, I know there must exist a cleaner way to do this.

UIDatePicker in UITableView, ensure Visibility

I have a static UITableView with several sections. In one table row I have a UIDatePicker. On touch the table cell expands and I can select the date. Fine so far. But if the table row is on the bottom of the page I need to manually scroll up to select a date. How can I ensure the datepicker to be in view like the calendar app does? Can you please point me into the right direction?
You can use this function:
func scrollToRowAtIndexPath(indexPath: NSIndexPath, atScrollPosition scrollPosition: UITableViewScrollPosition, animated: Bool)
Use it in
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// ...
var indexPathToJump = NSIndexPath(forRow: 0, inSection: 5)
tableView .scrollToRowAtIndexPath( indexPathToJump, atScrollPosition: .None, animated: true)
}
}
Use scrollToRowAtIndexPath:atScrollPosition:animated::
self.tableView.scrollToRowAtIndexPath(myIndexPath, scrollPosition: .None, animated: true)
I haven't tested this syntax, please let me know if it needs improved, but I know that the method is right. You want to use UITableViewScrollPosition.None so that it move the table view just enough that the row in question is in view:
UITableViewScrollPositionNone
The table view scrolls the row of interest to be fully visible with a minimum of movement. If the row is already fully visible, no scrolling occurs. For example, if the row is above the visible area, the behavior is identical to that specified by UITableViewScrollPositionTop. This is the default.
Available in iOS 2.0 and later.

Resources