Swift 3 | TableView ScrollToTop on reloadData with dynamic cell size - ios

I have a TableView with dynamic cell size :
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 20
I have cells with texts inside and, of course, the text Height can change in function of its content.
Sometimes I have only one line, sometimes I have more, it means sometimes my cells does 20 height, sometimes more.
I have an issue when I try to reload my tableview datas and scroll to the top.
This is what I do :
myTableViewDatas = newDatas
tableView.setContentOffset(CGPoint.zero, animated: false)
tableView.reloadData()
It is hard to show you this case but It doesn't scroll to Y = 0, it scrolls to Y = 100 or something like that. Because my cell size changes in function of the content to display.
If I remove dynamic size and do :
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 20
}
And still scroll to the top with :
myTableViewDatas = newDatas
tableView.setContentOffset(CGPoint.zero, animated: false)
tableView.reloadData()
==> This is working, I scroll to Y = 0
I think I tried anything :
scrollToRow
scrollRectToVisible
scrollsToTop
I still have the issue.
The only way this is working is if I delay the reloadData :
myTableViewDatas = newDatas
tableView.setContentOffset(CGPoint.zero, animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
tableView.reloadData()
}
This is working but It creates a "glitch" => It displays new datas then automatically scroll to top, this is disturbing for the user.
The other solution is to use "reloadSections" :
myTableViewDatas = newDatas
tableView.setContentOffset(CGPoint.zero, animated: false)
tableView.reloadSections(IndexSet(integer: 0), with: .none) // I have only one section
It works too but it is also creating a "glitch", this is like TableView is reloaded with an animation (even if I set .none) where cells displayed are reduced / enlarged in function of new datas.
I really can't find a "proper" solution to do this, does anyone as already encountered this issue ? TY

Well, sometimes you search complicated solutions and It is simple in fact.
I did :
- reload first
- scrollToIndexPath 0 second
-> It works
myTableViewDatas = newDatas
tableView.reloadData()
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)

I also have the same issue. [UITableView reloadData] doesn't work how we expected.
It doesn't layout tableView from the cleanslate.
Reloads everything from scratch. Redisplays visible rows. Note that this will cause any existing drop placeholder rows to be removed.
It says redisplays visible rows. So this is what I did.
data = nil; // clear data.
[tableView reloadData]; // reloadd tableView so make it empty.
[tableView setNeedsLayout]; // layout tableView.
[tableView layoutIfNeeded];
data = the data; // set the data you want to display.
[tableView reloadData]; // reload tableView

Related

Table view reload row at index path issue

Apple doc say : reloadRowsAtIndexPaths:withRowAnimation:
:
Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.
My problem scenario is that i want to update a label in a cell on button click and also update the layout (i.e. a new view of the cell is being added or removed on a condition applied). if reloadRowsAtindexPath is called on the button click then the tableview randomly scroll down to some other row in the tableview. If only the cell content is updated on button click then the layout is not updated properly.
If anybody has faced the same issue with the reload?
Well this turned out trickier than I expected.
Here's how I implemented it. I am not really sure if this is a good way or not so take it with a grain of salt. Find link to the project below.
You need to know two things:
The default/normal cell height (which is basically the estimated height of cell).
Increase in height of cell after the view has been added to the cell (in my example I have taken the height to be 200).
When you tap on a button which is supposed to add the subview you call a completion handler passing the indexPath and heightToBeAdded(200 in this example) as parameters.
The completion will look something like this:
self.indexPath = iPath
self.cellHeight = self.defaultCellHeight + heightToBeAdded
UIView.beginAnimations("aaa", context: nil)
UIView.setAnimationDuration(1)
self.tableView.beginUpdates()
self.tableView.reloadRows(at: [iPath], with: .none)
self.tableView.endUpdates()
UIView.commitAnimations()
if heightToBeAdded > 0.0 {
self.addCellSubviews()
}
The iPath is the same indexPath that you sent a parameter. The cell height is calculated from the two things I have described above.
When endUpdates() calls the delegate and datasource methods would encounter the following method:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let iPath = self.indexPath else { return UITableViewAutomaticDimension }
if indexPath == iPath {
return cellHeight
}
else {
return UITableViewAutomaticDimension
}
}
This will increase the cell height. But what about the actual subview that needs to be added?
For that we have addCellSubviews() which is actually a completion block that is executed inside the UITableViewCell subclass. Call this only after end updates because by that time cell height will be calculated properly.
let yView = UIView()
yView.backgroundColor = .yellow
yView.translatesAutoresizingMaskIntoConstraints = false
self.addAsSubview(sView: yView)
NSLayoutConstraint.activate([
yView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 8),
yView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
yView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -275),
yView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8)
])
UIView.animate(withDuration: 1, animations: {
self.layoutIfNeeded()
})
Here is the result -
Link to the project
Please note there are some caveats(like tapping one cell closes the other) which I am sure you will be able to work out.
This was an issue with the estimated row height. changing the value to some other estimate fixed the issue of unwanted scrolling.
Mentioned in apple docs: "Additionally, try to make the estimated row height as accurate as possible. The system calculates items such as the scroll bar heights based on these estimates. The more accurate the estimates, the more seamless the user experience becomes."

Sync animation for scroll in collection view within table view

I have collection views (in plural) inside a table view of many sections. Just so we're clear, a single table view with many sections with only one row each being that row an individual collection view.
All set up is working just fine, the data is well divided and delegates are all wired up recognizing everything they need to recognize. My problem is kind of simple but difficult at the same time: I want to scroll to specific collection view's position whenever I need to find a specific cell in animated fashion.
So far I'm able to jump with no problem to both table section (indexPath.section) and collection item (indexPath.row). The issue arises when I need to scroll (simultaneously) with animation.
My findings so far
I'm only able to achieve my current goal deactivating scroll animations for UITableView (UICollectionView can perform well with/out it)
Whenever I set UITableView selectRow or scrollToRow animation flags to true then the app crashes (99% sure this happens because I'm trying to access and "invisible" section due to the animation hasn't shown it yet).
Relevant snippets of code
#IBOutlet weak var albumTableView: UITableView!
#IBOutlet weak var stickersCollectionView: UICollectionView!
func locateCell() {
...
let stickerIndex = methodThatReturnsExactIndex()
let sectionIndex = IndexPath(row: 0, section: stickerIndex.section)
albumTableView.selectRow(at: sectionIndex, animated: false, scrollPosition: .top)
let rowIndex = IndexPath(item: stickerIndex.row, section: 0)
stickersCollectionView.scrollToItem(at: rowIndex, at: 0, animated: true)
}
I was thinking in experiment with the UIScrollViewDelegate (detecting when the tableview and the collectionview stopped in order to perform the scrolling) but that would imply spreading global variables around the code and experience tough me that's just racing conditions waiting to happen. Any help will be appreciated.
First Scroll your tableView to that specific index with/without animation. This will make that cell visible now get your cell by providing that indexPath so you could access the collectionView object inside your tableViewCell. Then ask you collectionView to scroll to specific indexPath with/without animation.
Take another global bool to store that tableView is begin scrolling. Also store both indexPath used for collection and tableView and use tab
tableViewIsScrolling = true
let yourSelectedIndexPathForTableView = IndexPath(row: 0, section: 4)//store it globally
let yourSelectedIndexPathForCollectionView = IndexPath(row: 10, section: 0)//store it globally
tableView.scrollToRow(at: yourSelectedIndexPathForTableView, at: .middle, animated: false)
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if tableViewIsScrolling {
tableViewIsScrolling = false
//Perform your scrolling of CollectionView
guard let yourCell = tableView.cellForRow(at: yourSelectedIndexPathForTableView) as? YourCell else {return}
yourCell.collectionView.scrollToItem(at: yourSelectedIndexPathForCollectionView, at: .centeredHorizontally, animated: true)
}
}

TableView scrollToRow is blocking the main ui thread

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

Scroll to last element of TableView take too much time

I'm trying to simulate a Whatsapp Chat any cell will have an image (for tail of the bubble), a bubble which is just View with color and some corner radius and a label which will represent the text of the message.
I've put a print before and after the call
self.messagesTableView.reloadData()
Once the after print is called tableView keeps some time doint I don't know what till the data is shown. And same happens with Insert row at indexpath, it takes some time till show the insert animation.
func displayMessages(viewModel: GroupChatMessages.GetChatMessages.ViewModel) {
let displayedMessage = viewModel.displayedMessages
print ("i'm here!")
messages = displayedMessage!
//self.messagesTableView.performSelectorOnMainThread(Selector("reloadData"), withObject: nil, waitUntilDone: true)
self.messagesTableView.reloadData()
print ("i'm here2!")
firstTime = false
self.setVisible(hiddenTableView: false, hiddenChatLoader: true)
self.scrollToLastMessage(false)
self.messagesLoaded = true
}
I've tried to do dispatched with queue, and the commented line before reloadData(), but nothings works and nothing represent a significative time.
Maybe could be for the image of the bubble? I don't know. I have this image saved on Assets, so I'm not downloading it from internet.
self.setVisible just hide the loader and show the tableView but I've tried too moving it up and nothings changes. Any further information you need let me know. Thanks!
EDIT:
Well I've seen that the problem comes from the scroll to last cell, this is where it takes the major part of the time.
func scrollToLastMessage(animated: Bool) {
let section = 0
let lastItemIndex = self.messagesTableView.numberOfRowsInSection(section) - 1
let indexPath:NSIndexPath = NSIndexPath.init(forItem: lastItemIndex, inSection: section)
self.messagesTableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: animated)
self.scrollDownButton.hidden = true
}
There is a posibility to optimize that scroll, because I have to do a Scroll because once the data is loaded, the first I've see is the top row of the tableView, but I would like to see the bottom one (last). Thanks!
methods like reloadData() should be considered as UI methods and it's mandatory to call them in main thread:
DispatchQueue.main.async { tableView.reloadData() }
It's better not to use reloadData() function unless a significant amount of cells need to refresh or data source has been changed instead use this method to add new rows:
tableView.insertRows(at: [IndexPath], with: UITableViewRowAnimation)
and for refreshing cell:
tableView.reloadRows(at: [IndexPath], with: UITableViewRowAnimation)
also if the cell has a considerable amount of images and rendering, use this code to make scrolling faster:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
// ADD THESE TWO LINE
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
}
Using these ways will boost loading speed significantly
Finally the solution that I've found to avoid dying while waiting scrolling to last element any single time, is swapping orientation of table
tableView.transform = CGAffineTransformMakeRotation(-M_PI);
cell.transform = CGAffineTransformMakeRotation(M_PI);
Now headerView and footerView are reversed. For exemple, if you would like insert rows at (visually) at the bottom of the TableView with this configuration you should add it at position 0 forRow: 0 atSection: "WhereYouAre". This way when you add new element, no scroll is needed, because scroll is automatically. Amazing and strange answer IMHO.
I've found this solution here:
Solution Link
#Christos Hadjikyriacou solved there.

Custom UIViewController transition where UITableViewCell grows to full screen and back?

I haven't been able to find any examples of this online. How can I achieve the following effect? Instead of the standard slide-to-left effect when tapping a table row, I'd like the view controller transition animation to look like the following:
User taps a cell in the table
The cell starts growing to fill the screen, pushing other rows above and below it "offscreen".
As the cell grows, cells elements (text, images, etc.) cross-fade into the new view's contents until the new view completely fills the screen.
I'd like to also be able to interactively transition back into the table view by dragging up from the bottom edge, such that the reverse of the above is achieved. i.e. view starts shrinking back into a normal table view cell as the "offscreen" cells animate back into position.
I've thought about taking a snapshot of the table and splitting it up at the points above and below the cell, and animating these snapshots offscreen as part of a custom view controller transition. Is there a better way? Ideally I'd like to not take snapshots, as I may want to have animations, etc., still happening in the table view rows as they fade offscreen.
First thing first, i have seen your post today and his is written in swift programming language.
the follwing code is to expand the selected cell to the full screen, where the subviews will fade out and background image will expands.
First complete cellForRowAtIndexPath, then in didSelectRowAtIndexPath,
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
isSelected = true
selectedCellIndex = indexPath.row
tableView.beginUpdates()
var cellSelected: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
cellSelected.frame = tableCities.rectForRowAtIndexPath(indexPath)
println("cell frame: \(cellSelected) and center: \(cellSelected.center)")
let newCell = cellSelected.frame.origin.y - tableOffset
println("new cell origin: \(newCell)")
var tempFrame: CGRect = cellSelected.frame
variableHeight = cellSelected.frame.origin.y - tableOffset
tempFrame.size.height = self.view.frame.height
println("contentoffset: \(tableView.contentOffset)")
let offset = tableView.contentOffset.y
println("cell tag: \(cellSelected.contentView.tag)")
let viewCell: UIView? = cellSelected.contentView.viewWithTag(cellSelected.contentView.tag)
println("label: \(viewCell)")
viewCell!.alpha = 1
UIView.animateWithDuration(5.0, delay: 0.1, options: UIViewAnimationOptions.BeginFromCurrentState, animations: {
tableView.setContentOffset(CGPointMake(0, offset + self.variableHeight), animated: false)
tableView.contentInset = UIEdgeInsetsMake(0, 0, self.variableHeight, 0)
tableView.endUpdates()
cellSelected.frame = tempFrame
viewCell!.alpha = 0
println("contentoffset: \(tableView.contentOffset)")
}, completion: nil)
}
then update your heightForRowAtIndexPath, as
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if isSelected && (selectedCellIndex == indexPath.row) {
return self.view.frame.height
}else {
return 100
}
}
Ignore this if already solved. and this might be helpful to others.

Resources