TableView scrollToRow is blocking the main ui thread - ios

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

Related

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

Swift 3 | TableView ScrollToTop on reloadData with dynamic cell size

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

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.

How to execute code after an animation without using the completion block?

I would like to execute some code after a built-in animation is completed.
I have a UITableView with a lot of cells/rows. Sometimes, when I do some operations and then I need to scroll to the top of my tableView. For that I use :
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: true)
But I need to perform some code once the top is reached, such a simple select and deselect of the 1rst row.
I implemented func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) from the UIScrollViewDelegate. It works fine (more or less, sometimes the animation isn't really smooth and we don't see the select/deselect "animation") except when I am already at the top of the tableView, then scrollViewDidEndScrollingAnimation isn't called.
So is there a way to execute some code once scrollToRowAtIndexPath:atScrollPosition:animated has been called?
EDIT:
When I'm talking about doing some operations, I'm talking about moving a row using moveRowAtIndexPath:toIndexPath from UITableView.
So when scrolling is needed it's fine, both animations take about the same amount of time. But when no scroll is needed, then the execution of the code I want to do after the animation, starts at the same time than the animation
You can use this Swift code which I adapted from an old Objective-C answer to scroll the view.
// First, test whether the tableView needs to scroll to the new position
var originalOffset = tableView.contentOffset.y;
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: false)
var offset = tableView.contentOffset.y;
if (originalOffset == offset) {
// No animation is needed since its already there
doThingAfterAnimation();
} else {
// We know it will scroll to a new position
// Return to originalOffset. animated:NO is important
tableView.setContentOffset(CGPointMake(0, originalOffset), animated: false);
// Do the scroll with animation so `scrollViewDidEndScrollingAnimation:` will execute
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: UITableViewScrollPosition.Top, animated: true)
}
And then of course:
func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
doThingAfterAnimation();
}

UITableView jumping to top on endUpdates while typing inside a cell on iOS 8 auto height [duplicate]

This question already has answers here:
reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling
(22 answers)
Closed 4 years ago.
I am using iOS 8 self-sizing cells (tableView.rowHeight = UITableViewAutomaticDimension and tableView:estimatedHeightForRowAtIndexPath:) combination. It works great until I start typing something inside a cell. When I'm typing, I'm calling beginUpdates/endUpdates pair to resize my cell (text view grows as I type and shrinks as I delete) but each call results in a bad jump to top of the table view. If I remove the beginUpdates/endUpdates pair then it doesn't jump but my cell doesn't resize as I type.
Here is a demonstration of the issue:
How can I get my cell to resize correctly as I type while not jumping to the top of the table view? I am only targeting iOS >= 8 so I don't need any kind of iOS 7 compatibility.
I was implementing the exactly the same thing for chat app and getting the same issue as you are getting now. This thing helped me out. Let me know if this works for you too.
UIView.setAnimationsEnabled(false)
tableView.beginUpdates()
cell.textView.scrollRangeToVisible(NSMakeRange(cell.textView.text.characters.count-1, 0))
tableView.endUpdates()
UIView.setAnimationsEnabled(true)
tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: false)
I found solution from Manoj Aher to work fine - text doesn't jump at all. But in my case I had to extend for situations when user might type much more text (so that the table cell is bigger than the visible part of the table) and then returns to edit this text somewhere at the beginning or in the middle. So I had to scroll the table cell to Top or Middle depending on where the user is typing to keep the typing position visible.
First, remember the indexPath for the cell in any convenient way. For example:
var cellIndexChosenForEdition: NSIndexPath
...
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
cellIndexChosenForEdition = indexPath
...
}
In shouldChangeTextInRange or any other method which is called when user types (shown here method will be called when your view controller conforms to UITextViewDelegate protocol):
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
...
UIView.setAnimationsEnabled(false)
MyTableView.beginUpdates()
MyTableView.endUpdates()
UIView.setAnimationsEnabled(true)
if let textStartPosition: UITextPosition = textView.selectedTextRange?.start {
let cursorPosition = textView.offsetFromPosition(textView.beginningOfDocument, toPosition: textStartPosition)
if textView.text.characters.count - cursorPosition < 170 {
table_create_issue.scrollToRowAtIndexPath(cellIndexChosenForEdition, atScrollPosition: .Bottom, animated: false)
} else if cursorPosition > 200 {
table_create_issue.scrollToRowAtIndexPath(cellIndexChosenForEdition, atScrollPosition: .Middle, animated: false)
} else {
table_create_issue.scrollToRowAtIndexPath(cellIndexChosenForEdition, atScrollPosition: .Top, animated: false)
}
}
...
}
*constants 200 and 170 should be changed to fit your concrete situation
**I didn't use scrollRangeToVisible because my textView height is always equal to cell height and never scrolls.
I'm having the same problem, and have been able to minimize (but not eliminate) the jumping by only doing the resize/update notification when the intrinsic size of the content has actually changed, e.g.:
Swift
var textViewHeight: CGFloat = 0.0 // Set in viewWillAppear as below
...
func textViewDidChange(textView: UITextView) {
let newHeight = textView.intrinsicContentSize().height
if textViewHeight != newHeight{
textViewHeight = newHeight
tableView.beginUpdates()
tableView.endUpdates()
}
}
Objective-C
CGFloat textViewHeight = 0.0; // Set in viewWillAppear as below
...
(void)textViewDidChange:(UITextView *)textView {
CGFloat newHeight = [textView intrinsicContentSize].height;
if (textViewHeight != newHeight){
textViewHeight = newHeight
[tableView beginUpdates];
[tableView endUpdates];
}
}
Are your reseting frame of Table View Cell on text begin editing? If yes, then you can check your variables sometimes due to memory allocation variables deallocate. Use break points and check variable values that can not be change according your requirement.
Auto layout have some drawback associated with it. When you start typing it jump because all other cell are adjusting their size so it will be better if you calculate the dimensions of cell on runtime only.

Resources