UITableView dynamic cell height scroll to top after calling reloadData - ios

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.

Related

How to fetch data from API and update cells and sections when scrolling up the tableView using willDisplay cell method?

I am using tableView to show the data from API and i have sections and cells inside. When user scrolls down, my algorithm fetching the data according the date (for example today's data from API in on section, and when scrolling down, it fetches data from API again for new section with number of cells according to API) correctly, but i need to fetch previous date from API as scrolling down, when user scrolls up, it should show preview day. I have seen many answers for scroll down, but not up. Here is my little chunk of code:
if order == 0 {
let indexSet = IndexSet(arrayLiteral: 0)
ByPassArray.insert(rootJSON, at: 0)
DispatchQueue.main.async {
self.scheduleTableView.insertSections(indexSet,
with: 0)
self.scheduleTableView.reloadData()
}
}
else {
ByPassArray.append(rootJSON)
DispatchQueue.main.async {
self.scheduleTableView.reloadData()
}
}
I don't know if I get it right but you might want to implement the method
optional func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
in your UITableViewDelegate and check for the cell at indexPath.row == 0
If you know that you're about to present the first cell of the tableview then you might want to start your API call and then do whatever you have to do.

How to reset indexPath.row value to zero when reloading data?

I have a UITableView that has about 30 items. Here's an overview of how my tableview works: In order to enable infinite scrolling and a small memory size, I calculate the scrolling direction, speed, and I remove the top 10 items and add 10 items to the bottom (the same goes for scrolling up the other way). If I were to scroll to an item, the indexPath.row stays at that same item when I attempt to reloadData() with a new set of 30 items.
I have tried creating my own index to use, but it has a lot facets to it that would make it pretty complicated. I'm looking to see if I can just reset indexPath.row.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
item = items[indexPath.row]
}
Sometimes, I would like to jump to another set of items that is a few pages down. So, I then replace the current array of items with a new 30 items. However, when I have already scrolled down a little (let's say I'm on the 15th cell), and I then jump to a new set of 30 items, the very first cell to show in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell is the 15th cell. indexPath.row = 15... Why is indexPath.row not updating to 0? Is there a way to reset indexPath.row to 0 when I fill my array with a new set of items and call reloadData()?
Apparently, manually setting the indexPath is not possible. Instead, I just scroll the tableView to the top to ensure that it starts at the very first element.
tableView.scroll(to: .top, animated: false)

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..!!!

Hide unhide Static Table View Cells with Swift

I am trying to make a '''Static Table''' with some cells that are expandable and collapsing . I override the ''': WithAnimation:)''' which should provide me some control on the animation transition. Unfortunately, the cells returned are blank, I don't mean empty, but the cells have disappeared and their location is left blanc in the table. When I try using the '''reloadSections( )''' rather, the whole section is also returned blank. After some recherche , I start to suspect that '''reloadRowAtIndexPath(​: WithAnimation:)''' and '''reloadSections( )''' works with table associated with a data source.
Am I right? If yes, how can I control animation of the cell I want to hide/unhide so that the transition is smooth?
Thanks (edited)
To get a smooth animation, simply reload the tableView with
tableView.beginUpdates()
tableView.endUpdates()
this will call the heightForRowAtIndexPath automatically and render a smooth transition.
To expand on irkinosor's answer. In this example I have 1 section with 3 static rows. When tableView is initially loaded, my UISwitch is off and I only want the first row (row 0) be visible (hide rows 1,2). When the UISwitch is on, I want to show (rows 1,2). Also, I want smooth animation having the rows accordion to hide or show.
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row > 0 && toggleSwitch.isOn == false {
return 0.0 // collapsed
}
// expanded with row height of parent
return super.tableView(tableView, heightForRowAt: indexPath)
}
In my switch action I initiate the tableView refresh
#IBAction func reminderSwitch(_ sender: UISwitch) {
// to initiate smooth animation
tableView.beginUpdates()
tableView.endUpdates()
}
You can obviously add more logic to the heightForRowAt function if you have multiple sections and more interesting row requirements.

Why do UITableView actions only work on visible cells

I'm working with a UITableView which is a list of friends (I have 20 friends). I have, on top of those 20 friends cells, a unique cell in another section called "ALL FRIENDS" which allows me to select all my friends in one click.
When this "ALL FRIENDS" cell is selected, this code is called :
var index = 0
for friend in self.friendsList {
var indexPath = NSIndexPath(forRow: index, inSection: 1)
self.tableView.selectRowAtIndexPath(indexPath, animated: true, scrollPosition: UITableViewScrollPosition.None)
index += 1
}
Everything works fine for the visible cells. When a cell is selected the background color changes but this only works for visible cells.
Any ideas why ?
Because UITableView is dynamically displayed and invisible cells are not there.
UITableView basically dequeues a cell that's about to disappear and appends it where you're scrolling. To select all of the cells, you have to do it differently:
You'll have to set a global variable to true, let's say var allSelected = true.
Then call self.tableView.reloadData()
Which will be reflected in tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell by doing cell.selected = true.
That will ensure that all of the future displayed cells will be selected as well.
So Michal's answer is great, but I would like to give you an alternative: you can create that tableview without dequeuing and reusing cells, so the cells remain alive at all points.
You can do this by creating individual cells, and adding them to an array
var array = [UITableViewCell]
for friend in self.friendsList {
var cell = UITableViewCell()
// customize your cell however you want, or use custom nibs
array.append(cell)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) {
return array[indexPath.row]
}

Resources