I have a tableview (style - grouped) which contains more rows than can fit into its frame so you have to scroll to see the last row. When I delete the last row the tableview scrolls its content down so the new last row is at the bottom of the frame (kind of fill the empty space).
How can I prevent a UITableView from scrolling after I delete the last row in it? I want it to keep the empty space and do not scroll automatically.
I have tried to set content insets but it doesn't seem to prevent scrolling unfortunately.
EDIT: My code for removing the row:
func removeLastRow() {
let indexPath = IndexPath.init(row: self.messages.count - 1, section: 0)
self.messages.removeLast()
CATransaction.begin()
self.tableView.beginUpdates()
CATransaction.setCompletionBlock { () -> Void in
// do stuff, add new rows
}
self.tableView.deleteRows(at: [indexPath], with: .left)
self.tableView.endUpdates()
CATransaction.commit()
}
Retreive the cells size before deleting by calling tableView:heightForRowAt: and after deleting, set the vertical content offset of your table view manually to the current vertical content offset, plus the height of the cell.
let deletedCellHeight = tableView.heightForRowAt(indexPath)
Then:
tableView.contentOffset.x += deletedCellHeight
Related
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 :)
I have custom layout with fullscreen cells. When removing cell from the left (it's not visible at the time), UICollectionView jumps to the next cell.
It's like current cell was at index 4 and when cell on the left removed the next cell has index 4 now and immediately scroll to the next cell.
Describing in 3 steps (A is cell that need to be fullscreen, x will be removed, o other cells, large letter is fullscreen):
ooooAoo
oooxAoo
oooaOo
But must keep this oooAoo
Here is my solution if it can help to anybody, did not found how to achieve desired offset natively, so just scrolling contentOffset to the desired position right after reloadData():
var currentCell: MyCollectionViewCell? {
return (visibleCells.sorted { $0.frame.width > $1.frame.width }.first) as? MyCollectionViewCell
}
//-----------------------------------------
//some model manipulating code, removing desired items here...
let currentList = currentCell?.parentList
reloadData()
if let list = currentList, let index = self.lists.firstIndex(of: list) {
self.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredHorizontally, animated: false)
}
currentCell is computed property, returns optional middle cell.
Detect which cell is the largest, because of custom flowlayout logic.
parentList is the model item, I can compare cell by it to make life
easier. I check which list was attached to the cell before
reloadData().
I'm trying to update some constraints on a cell depending on the scroll progress inside my TableView. The goal is to recreate this effect : https://youtu.be/VMyNHq3CO04?t=416 (at 6:56)
I'm currently having all my cell with spaces and rounded corner. For now, I get the position of the offset of the scrollView of my tableView and run the following function :
func updateCellState(position: CGFloat) {
let numberOfItems = tableView.numberOfRows(inSection: 0)
for i in 0..<numberOfItems {
guard let cell = tableView(tableView, cellForRowAt: IndexPath(row: i, section: 0)) as? FMReceiptReviewCellWithContextualActions else {
break
}
tableView.beginUpdates()
self.stretch = position / 1000 > 1 ? 1 : position / 1000
cell.delegate?.updateCellState(position: position)
cell.setNeedsLayout()
cell.layoutIfNeeded()
tableView.endUpdates()
}
}
The updateCellState take the position of the scrollView to update my constraint proportionally.
But I've some major issues with this code :
In term of performance, the function is called way to often because of the scrollView (even when calling the updateCellState only by 30,40,... offset point)
My cells are not always updated and they need to be dequeue to be reload and display my cells with new parameters (a reloadData() of the tableView work, but not suitable for a smooth flow)
Do you have any idea how I could replicate the effect I mentioned above.
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)
How to animate the footer view a table view when the UItableView reloads?
I am trying to append an element to the tableView and when the table view reloads the footer view should come with some animation. And also if the table view contents exceeds the screen size the footer view should stick with bottom of the screen. Here is my try:
tableView.beginUpdates()
self.familyAModelArray.append(self.childAModelArray)
tableView.endUpdates()
Note: Here i want to append an object to an array which is the content of the UITableView and reload the TableView. Thanks in advance!
To animate the insert action, you need to call insertRowsAtIndexPaths method with row animation.
After adding table cell elements to your array, you must calculate the index paths for the new cells to be displayed, and create an array of them, like this
let indexes = [IndexPath(row: 0, section: 0), IndexPath(row: 1, section: 0)]
Then simply call the following method to animate the insertion
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths(indexes, withRowAnimation: .Fade)
tableView.endUpdates()
If you want the footer view to stick on the bottom of your screen, then it shouldn't be a footer view. it should be a custom UIView that is pinned to your view controller's view. Whenever you call a reloadData() on your tableView, you can likewise call a present/animate function on your new custom view that you pinned to the bottom & animate it on / off the screen.
I have solved this by using animation on UIView when the UITableView reloads after appending the rows.
UIView.transition(with: tableView, duration: 0.2, options: .curveEaseIn, animations: {self.tableView.reloadData()}, completion:{ (success) in
if success {
if self.familyAModelArray.count >= 3{
self.scrollToBottom()
}
}
})
And if length of my array is greater than 3 the footer view stick at the bottom by scrolling the UITableView to the top .
func scrollToBottom(){
DispatchQueue.global(qos: .background).async {
let indexPath = IndexPath(row: self.familyAModelArray.count-1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
This solution has given a nice look for my table view.