UITableViewCell has a choppy row insertion/deletion animation - ios

I'm currently deleting and inserting rows based on the content within a class which is called a content builder. An example of this can be seen below.
func stateDidChange(id: String, _ state: Bool) {
switch id {
case ConfigureExerciseContentBuilderKeys.accessory.rawValue:
exerciseProgramming.isAccessory = state
if state {
if let customIncrementsItemIndex = self.configureExerciseContentBuilder.getContent().firstIndex(where: { ($0 as? FormLabelTextFieldItem)?.id == ConfigureExerciseContentBuilderKeys.increment.rawValue }),
let syncedWorkoutsItemIndex = self.configureExerciseContentBuilder.getContent().firstIndex(where: { ($0 as? ExerciseSyncedWorkouts)?.id == ConfigureExerciseContentBuilderKeys.syncedWorkouts.rawValue }) {
self.configureExerciseContentBuilder = ConfigureExerciseContentBuilder(type: type, exerciseProgramming: exerciseProgramming)
let incrementsIndexPath = IndexPath(item: customIncrementsItemIndex, section: 0)
let syncedIndexPath = IndexPath(item: syncedWorkoutsItemIndex, section: 0)
let spacerIndexPath = IndexPath(item: syncedWorkoutsItemIndex + 1, section: 0)
self.tableView.beginUpdates()
self.tableView.deleteRows(at: [incrementsIndexPath, syncedIndexPath, spacerIndexPath], with: .automatic)
self.tableView.endUpdates()
}
} else {
self.configureExerciseContentBuilder = ConfigureExerciseContentBuilder(type: type, exerciseProgramming: exerciseProgramming)
if let customIncrementsItemIndex = self.configureExerciseContentBuilder.getContent().firstIndex(where: { ($0 as? FormLabelTextFieldItem)?.id == ConfigureExerciseContentBuilderKeys.increment.rawValue }),
let syncedWorkoutsItemIndex = self.configureExerciseContentBuilder.getContent().firstIndex(where: { ($0 as? ExerciseSyncedWorkouts)?.id == ConfigureExerciseContentBuilderKeys.syncedWorkouts.rawValue }) {
let incrementsIndexPath = IndexPath(item: customIncrementsItemIndex, section: 0)
let syncedIndexPath = IndexPath(item: syncedWorkoutsItemIndex, section: 0)
let spacerIndexPath = IndexPath(item: syncedWorkoutsItemIndex + 1, section: 0)
self.tableView.beginUpdates()
self.tableView.insertRows(at: [incrementsIndexPath, syncedIndexPath, spacerIndexPath], with: .automatic) // Insert into the index of where the accessory row was/is
self.tableView.endUpdates()
}
}
default:
break
}
}
The rows are successfully deleted and inserted based on the logic above and I used the automatic parameter so that the tableview can handle this in the best way.
It's also worth noting that within the tableviewcell autolayout is used on elements within them and also I've configured the row height within the tableview to be dynamic based on its contents.
But for some reason I seem to be getting a weird choppy animation when I delete or insert rows for the green box that you can see in the video attached. Does anyone have any pointers/ideas as to why this may be happening?
Link to video

Related

After delete rows table view has old indexes Swift

I have table view that delete some rows with following:
func deleteRows(_ indecies: [Int]) {
guard !indecies.isEmpty else { return }
let indexPathesToDelete: [IndexPath] = indecies.map{ IndexPath(row: $0, section: 0)}
let previousIndex = IndexPath(row: indecies.first! - 1, section: 0)
tableView.deleteRows(at: indexPathesToDelete, with: .none)
tableView.reloadRows(at: [previousIndex], with: .none)
}
In cellForRow i have cell that have "tap" closure like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard indexPath.row < presenter.fields.count else { return EmptyCell() }
let field = presenter.fields[indexPath.row]
switch field.cellType {
case .simple:
guard let model = field as? SimpleTextItem else { return EmptyCell() }
let cell = SimpleTextCell()
cell.setup(label: LabelSL.regularSolidGray(), text: model.text, color: Theme.Color.bleakGray)
return cell
case .organization:
guard let model = field as? OrganizationFilterItem else { return EmptyCell() }
let cell = OrganizationFilterCell()
cell.setup(titleText: model.title,
holdingNumberText: model.holdingNumberText,
isChosed: model.isChosed,
isHolding: model.isHolding,
isChild: model.isChild,
bottomLineVisible: model.shouldDrawBottomLine)
cell.toggleControlTapped = {[weak self] in
self?.presenter.tappedItem(indexPath.row)
}
return cell
}
}
When
cell.toggleControlTapped = {[weak self] in
self?.presenter.tappedItem(indexPath.row)
}
Tapped after rows deletion, index is pass is wrong (it's old). For example, i have 10 rows, i delete 2-3-4-5 row, and then i tap on 2 row (it was 6 before deletion). That method pass "6" instead of "2".
Problem actually was solved by adding tableView.reloadData() in deleteRows function, but, as you may assume smoothly animation gone and it look rough and not nice. Why is table still pass old index and how to fix it?
A quite easy solution is to pass the cell in the closure to be able to get the actual index path
Delare it
var toggleControlTapped : ((UITableViewCell) -> Void)?
Call it
toggleControlTapped?(self)
Handle it
cell.toggleControlTapped = {[weak self] cell in
guard let actualIndexPath = self?.tableView.indexPath(for: cell) else { return }
self?.presenter.tappedItem(actualIndexPath.row)
}
Side note: Reuse cells. Creating cells with the default initializer is pretty bad practice.
Be sure to update your data too, indexes are appearing from numberofrows function of table view

MoveRow method not animating when moving between sections

Im creating Shopping List app. I use 2 sections for separating bought items and Items that are not bought. I use moveRow method for moving rows between said 2 sections. This is the code for moving rows.
if indexPath.section == 0 {
self.shoppingItems.remove(at: indexPath.row)
self.shoppingItemsBought.append(item)
self.tableView.beginUpdates()
let fromIndexPath = NSIndexPath(row: indexPath.row, section: 0)
let toIndexPath = NSIndexPath(row: 0, section: 1)
self.tableView.moveRow(at: fromIndexPath as IndexPath, to: toIndexPath as IndexPath)
self.tableView.endUpdates()
} else {
self.shoppingItemsBought.remove(at: indexPath.row)
self.shoppingItems.append(item)
self.tableView.beginUpdates()
let fromIndexPath = NSIndexPath(row: indexPath.row, section: 1)
let toIndexPath = NSIndexPath(row: 0, section: 0)
self.tableView.moveRow(at: fromIndexPath as IndexPath, to: toIndexPath as IndexPath)
self.tableView.endUpdates()
}
The rows change their location but without animation. What am I missing?
One option is to use CATransaction around the moveRow. Put your changes inside the CATransaction block and on completion, force the reload. This will do the reload with animations.
Example:
CATransaction.begin()
CATransaction.setCompletionBlock {
tableView.reloadData()
}
tableView.beginUpdates()
// Update the datasource
if indexPath.section == 0 {
self.shoppingItems.remove(at: indexPath.row)
self.shoppingItemsBought.append(item)
}
else {
self.shoppingItemsBought.remove(at: indexPath.row)
self.shoppingItems.append(item)
}
// Get new IndexPath (indexPath is the old one, as in current)
let newIndexPath = IndexPath(row: 0, section: indexPath.section == 0 ? 1 : 0)
// Move the row in tableview
tableView.moveRow(at: indexPath, to: newIndexPath)
tableView.endUpdates()
CATransaction.commit()

TableView collapsing cell while scrolling to another: weird behavior

I have a tableView with chat bubbles.
Those bubbles are shortened if the character count is more than 250
If a user clicks on a bubble
The previous selection gets deselected (shortened)
The new selection expands and reveals the whole content
The new selections top constraint changes (from 0 to 4)
What I would like to achieve?
If a long bubble is selected already, but the user selects another bubble, I want the tableView to scroll to the position of the new selected bubble.
I'll share a video about it
Without this scrolling, the contentOffset remains the same and it looks bad.
(In the video: on the right)
Video:
Right: without the mentioned scrolling
Left: with scrolling
https://youtu.be/_-peZycZEAE
Here comes the problem:
On the left, you can notice that it is glitchy.
Random ghost cells are appearing for no reason.
Sometimes it even messes up the height of some bubbles (not in the video)
Why is it so?
Code:
func bubbleTappedHandler(sender: UITapGestureRecognizer) {
let touchPoint = sender.location(in: self.tableView)
if let indexPath = tableView.indexPathForRow(at: touchPoint) {
if indexPath == currentSelectedIndexPath {
// Selected bubble is tapped, deselect it
self.selectDeselectBubbles(deselect: indexPath)
} else {
if (currentSelectedIndexPath != nil){
// Deselect old bubble, select new one
self.selectDeselectBubbles(select: indexPath, deselect: currentSelectedIndexPath)
} else {
// Select bubble
self.selectDeselectBubbles(select: indexPath)
}
}
}
}
func selectDeselectBubbles(select: IndexPath? = nil, deselect: IndexPath? = nil){
var deselectCell : WorldMessageCell?
var selectCell : WorldMessageCell?
if let deselect = deselect {
deselectCell = tableView.cellForRow(at: deselect) as? WorldMessageCell
}
if let select = select {
selectCell = tableView.cellForRow(at: select) as? WorldMessageCell
}
// Deselect Main
if let deselect = deselect, let deselectCell = deselectCell {
tableView.deselectRow(at: deselect, animated: false)
currentSelectedIndexPath = nil
// Update text
deselectCell.messageLabel.text = self.dataSource[deselect.row].message.shortened()
}
// Select Main
if let select = select, let selectCell = selectCell {
tableView.selectRow(at: select, animated: true, scrollPosition: .none)
currentSelectedIndexPath = select
// Update text
deselectCell.messageLabel.text = self.dataSource[select.row].message.full()
}
UIView.animate(withDuration: appSettings.defaultAnimationSpeed) {
// Deselect Constraint changes
if let deselect = deselect, let deselectCell = deselectCell {
// Constarint change
deselectCell.nickNameButtonTopConstraint.constant = 0
deselectCell.timeLabel.alpha = 0.0
deselectCell.layoutIfNeeded()
}
// Select Constraint changes
if let select = select, let selectCell = selectCell {
// Constarint change
selectCell.nickNameButtonTopConstraint.constant = 4
selectCell.timeLabel.alpha = 1.0
selectCell.layoutIfNeeded()
}
}
self.tableView.beginUpdates()
self.tableView.endUpdates()
UIView.animate(withDuration: appSettings.defaultAnimationSpeed) {
if let select = select, deselect != nil, self.tableView.cellForRow(at: deselect!) == nil && deselectCell != nil {
// If deselected row is not anymore on screen
// but was before the collapsing,
// then scroll to new selected row
self.tableView.scrollToRow(at: select, at: .top, animated: false)
}
}
}
Update 1: Added Github project
Link: https://github.com/krptia/test2
I made a small version of my app, so that you can see and test what my problem is. I would be so thankful if someone could help to solve this issue! :c
First let's define what we mean by "without scrolling" - we mean that the cells more of less stay the same. So we want to find a cell that we want to be the anchor cell. From before the changes to after the changes the distances from the cell's top to the top of the screen is the same.
var indexPathAnchorPoint: IndexPath?
var offsetAnchorPoint: CGFloat?
func findHighestCellThatStartsInFrame() -> UITableViewCell? {
return self.tableView.visibleCells.filter {
$0.frame.origin.y >= self.tableView.contentOffset.y
}.sorted {
$0.frame.origin.y > $1.frame.origin.y
}.first
}
func setAnchorPoint() {
self.indexPathAnchorPoint = nil;
self.offsetAnchorPoint = nil;
if let cell = self.findHighestCellThatStartsInFrame() {
self.offsetAnchorPoint = cell.frame.origin.y - self.tableView.contentOffset.y
self.indexPathAnchorPoint = self.tableView.indexPath(for: cell)
}
}
we call this before we start doing stuff.
func bubbleTappedHandler(sender: UITapGestureRecognizer) {
self.setAnchorPoint()
....
Next we need to set the content offset after we are doing doing our changes so that the cell moves back to where it is suppose to.
func scrollToAnchorPoint() {
if let indexPath = indexPathAnchorPoint, let offset = offsetAnchorPoint {
let rect = self.tableView.rectForRow(at: indexPath)
let contentOffset = rect.origin.y - offset
self.tableView.setContentOffset(CGPoint.init(x: 0, y: contentOffset), animated: false)
}
}
Next we call it after we are done doing our changes.
self.tableView.beginUpdates()
self.tableView.endUpdates()
self.tableView.layoutSubviews()
self.scrollToAnchorPoint()
The animation can be a little strange because there is a lot going on at the same time. We are changing the content offset and the sizes of the cell at the same time, but if you place your finger next to the first cell that top is visible you will see it ends up in the correct place.
Try using this willDisplay api on UITableViewDelegate
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if (indexPath == currentSelectedIndexPath) {
// logic to expand your cell
// you don't need to animate it since still not visible
}
}
Replace your code for bubbleTappedHandler with this and run and check:
func bubbleTappedHandler(sender: UITapGestureRecognizer) {
let touchPoint = sender.location(in: self.tableView)
if let indexPath = tableView.indexPathForRow(at: touchPoint) {
if indexPath == currentSelectedIndexPath {
currentSelectedIndexPath = nil
tableView.reloadRows(at: [indexPath], with: .automatic)
} else {
if (currentSelectedIndexPath != nil){
if let prevSelectedIndexPath = currentSelectedIndexPath {
currentSelectedIndexPath = indexPath
tableView.reloadRows(at: [prevSelectedIndexPath, indexPath], with: .automatic)
}
} else {
currentSelectedIndexPath = indexPath
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { [weak self] in
let currentIndexPath = self?.currentSelectedIndexPath ?? indexPath
self?.tableView.scrollToRow(at: currentIndexPath, at: .top, animated: false)
})
}
}
You can use tableView?.scrollToRow. For example
let newIndexPath = IndexPath(row: 0, section: 0) // put your selected indexpath and section here
yourTableViewName?.scrollToRow(at: newIndexPath, at: UITableViewScrollPosition.top, animated: true) // give the desired scroll position to tableView
Available scroll positions are .none, .top, .middle, .bottom
This glitch is because of a small mistake. Just do these 2 things and the issue will be resolved.
Set tableView's rowHeight and estimatedRowHeight in `viewDidLoad
tableView.estimatedRowHeight = 316 // Value taken from your project. This may be something else
tableView.rowHeight = UITableView.automaticDimension
Remove the estimatedHeightForRowAt and heightForRowAt delegate methods from your code.

Swift3 collectionView 'attempt to delete item 1 from section 1, but there are only 1 sections before the update'

My app crashed due to 'attempt to delete item 1 from section 1, but there are only 1 sections before the update'
I found other articles said that the data source must keep in sync with the collectionView or tableView, so I remove the object from my array: alarms before I delete the item in my collectionView, this works on didSelectItemAtIndexPath. But I don't know why this doesn't work.
I also tried to print the array.count, it showed the object did remove form the array.
func disableAlarmAfterReceiving(notification: UILocalNotification) {
for alarm in alarms {
if alarm == notification.fireDate {
if let index = alarms.index(of: alarm) {
let indexPath = NSIndexPath(item: index, section: 1)
alarms.remove(at: index)
collectionView.deleteItems(at: [indexPath as IndexPath])
reloadCell()
}
}
}
}
func reloadCell() {
userDefaults.set(alarms, forKey: "alarms")
DispatchQueue.main.async {
self.collectionView.reloadData()
print("Cell reloaded")
print(self.alarms.count)
}
}
You are having single section so you need to delete it from the 0 section. Also use Swift 3 native IndexPath directly instead of NSIndexPath.
let indexPath = IndexPath(item: index, section: 0)
alarms.remove(at: index)
DispatchQueue.main.async {
collectionView.deleteItems(at: [indexPath])
}
Edit: Try to break the loop after deleting items from collectionView.
if let index = alarms.index(of: alarm) {
let indexPath = IndexPath(item: index, section: 0)
alarms.remove(at: index)
DispatchQueue.main.async {
collectionView.deleteItems(at: [indexPath])
}
reloadCell()
break
}

Close every other open UITableViewCell

I'm using Cristik's answer from this post: Drop-Down List in UITableView in iOS
The code works fine, but I am trying to create logic so when the user clicks on a closed cell, every other open cell closes. So if the Account cell was open, when the user clicks on the Event one, the event one opens and the Account one closes.
I tried the following logic:
for (var x = 0; x < displayedRows.count; x++) {
let view = displayedRows[x]
if (view.isCollapsed == false && x != indexPath.row){
let range = x+1...x+view.children.count
displayedRows.removeRange(range)
let indexPaths = range.map{return NSIndexPath(forRow: $0, inSection: indexPath.section)}
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
}
}
Here is the full function:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: false)
let viewModel = displayedRows[indexPath.row]
if viewModel.children.count > 0 {
let range = indexPath.row+1...indexPath.row+viewModel.children.count
let indexPaths = range.map{return NSIndexPath(forRow: $0, inSection: indexPath.section)}
tableView.beginUpdates()
if viewModel.isCollapsed {
displayedRows.insertContentsOf(viewModel.children, at: indexPath.row+1)
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
for (var x = 0; x < displayedRows.count; x++) {
let view = displayedRows[x]
if (view.isCollapsed == false && x != indexPath.row){
let range = x+1...x+view.children.count
displayedRows.removeRange(range)
let indexPaths = range.map{return NSIndexPath(forRow: $0, inSection: indexPath.section)}
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
}
}
}
else {
displayedRows.removeRange(range)
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
}
tableView.endUpdates()
}
viewModel.isCollapsed = !viewModel.isCollapsed
lastIndex = indexPath.row
}
I've tried variations of this code as well but for some reason keep getting the following error: Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert row 6 into section 0, but there are only 6 rows in section 0 after the update'
I know that this is a datasource problem but I can't seem to find a solution. Any solution/or alternative way to accomplish this would be appreciated! Thanks

Resources