Issue while scrolling to UITableView bottom - ios

I have a chat application ,where in when I press the send button the table gets reloaded with new row and I am calling scroll to bottom function to scroll to the newly added cell. The problem is that my textview text does not get emptied until the tableview scrolls to the bottom. Is there any other way to perform these actions so as to reduce the time delay?
Scroll to botton code:
self.tableView.reloadData()
let section: Int = numberOfSections(in: self.tableView) - 1
let item: Int = tableView(self.tableView, numberOfRowsInSection:section) - 1
let lastIndexPath = IndexPath(item: item, section: section)
self.tableView.scrollToRow(at: lastIndexPath, at: .bottom, animated: false)
When I remove the above code, the textview gets emptied instantly
The issue :

Make sure that you call reloadData() on the main thread. Doing it in a background thread will often cause graphical delays and may even crash the application.
Also, where do you empty the textview?

Related

Table view batch updates while scrolling

I've built a pagination mechanism for my table view so when the user scrolls to the last row in section the app loads additional content if possible. When additional content gets loaded, I perform a batch update on my table view like this:
guard
let sectionIndex = self.sections.firstIndex(of: .itemsSection),
!loadedItems.isEmpty
else { break }
let previousNumberOfRows = self.tableView.numberOfRows(inSection: sectionIndex)
let additionalIndexPaths = self.generateIndexPaths(
startIndex: previousNumberOfRows,
count: loadedItems.count,
section: sectionIndex
)
self.tableView.performBatchUpdates({
self.items.append(contentsOf: loadedItems)
self.tableView.insertRows(at: additionalIndexPaths, with: .automatic)
}, completion: nil)
and here's a function that generates index paths to insert new rows at:
private func generateIndexPaths(startIndex: Int, count: Int, section: Int) -> [IndexPath] {
var indexPaths: [IndexPath] = []
for row in startIndex..<startIndex + count {
indexPaths.append(IndexPath(row: row, section: section))
}
return indexPaths
}
It works perfectly fine until the update happens while table view is scrolling. Additional content is getting loaded so fast that the scroll animation have no time to finish. It starts to jump and additional cells pop with a broken animation. Since my table view uses UITableView.automaticDimension, I thought it was because of the wrong estimated height for newly created cells, so I implemented tableView(_:estimatedHeightForRowAt:) -> CGFloat method and gave those cells a pretty accurate height value. But it didn't help fixing the jumps. After trying all possible UITableView.RowAnimation fitting my need, I decided to completely disable the update animation. Here's how I'm currently updating the table view:
self.items.append(contentsOf: loadedItems)
UIView.setAnimationsEnabled(false)
self.tableView.insertRows(at: additionalIndexPaths, with: .none)
UIView.setAnimationsEnabled(true)
This approach gets rid of the update animation and at the same time introduces another problem: it feels like the table view is unresponsive during the update animation, like if I set isUserInteractionEnabled = false for a second. So what is the best way to update the table view with new rows with no animation and jumps? Thanks in advance :)

iOS UITableView scrolling insets while filtering

I have some problem with UITableView scrolling. This is my simple screen where I have UITextField and UITableView with cells. When user type something in UITextField app filters list of items and reloads UITableView. And it is working as expected.
On the right screen you can see how UITableView looks when I scroll. Cells go under UITextField.
Let's assume I scrolled some cell and If I start to type something in UITextField I got list of filtered cells but some of them are under UITextField and I can't scroll them down.
By default the table's scroll offset doesn't change when you reload the table with new set of row data. You need to scroll back to top after reloading the table :
self.tableView.scrollToRow(at: IndexPath(row: 0, section: 0),
at: .top,
animated: true)
and if you don't have a default place-holder cell to represent "No Matching Results" when there are no results and thus no cells to represent at Index (0,0) then add following condition before the above code to safeguard a crash:
guard cellResults.count > 0 else { return }
First of all your UITextField contained on tableview header view? UITableView need scrolling with table or not?
If your need always show first cell when user input something in UITextField, i recommend it with RxSwift like this
textField
.rx.text
.orEmpty
.debounce(0.1, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.subscribe(onNext: { [unowned self] query in
//TODO: Do something with you table, for example
// self.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
// or update tableView.
})
.disposed(by: disposeBag)

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.

Resources