I'm developing an app which has a main tableview in home screen. When I tap on a certain type of cell of the tableview, I do a little animation and then I delete the cell. I tried the behaviour on all simulator. Only on 4,7" and 5,5" I get a crash when I try to delete a certain row, systematically. I can't understand the error, since Xcode only says: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
This is my code:
tableView.deselectRow(at: indexPath, animated: false)
let incomingReminderTableViewCell = cell as! IncomingReminderTableViewCell
self.tableView.isUserInteractionEnabled = false
incomingReminderTableViewCell.completeReminderAt(indexPath: indexPath) { (indexPath) in
let selectedReminder = self.incomingReminders.reversed()[indexPath.row-1]
if CoreDataManager.sharedInstance.delete(selectedReminder) {
UserNotificationManager.decreaseBadge()
UserNotificationManager.deleteNotificationsWith(identifiers: [selectedReminder.idNotification])
self.incomingReminders = self.incomingReminders.reversed()
self.incomingReminders.remove(at: indexPath.row-1)
self.incomingReminders = self.incomingReminders.reversed()
if self.incomingReminders.isEmpty {
UIView.animate(withDuration: 0.2, animations: {
incomingReminderTableViewCell.alpha = 0.0
}, completion: { (disappeared) in
self.tableView.reloadRows(at: [indexPath], with: .fade )
self.tableView.isUserInteractionEnabled = true
})
} else {
UIView.animate(withDuration: 0.2, animations: {
incomingReminderTableViewCell.alpha = 0.0
}, completion: { (disappeared) in
self.tableView.isUserInteractionEnabled = true
self.tableView.deleteRows(at: [indexPath], with: .right) // HERE THE CRASH
self.incomingReminders = CoreDataManager.sharedInstance.getIncomingReminders()
if self.incomingReminders.count == 5 {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { (timer) in
self.tableView.insertRows(at: [IndexPath(row: self.incomingReminders.count, section: 0)], with: .fade)
timer.invalidate()
})
}
})
}
} else {
self.tableView.isUserInteractionEnabled = true
}
}
When app crash I have indexPath = (row: 3, section: 0), that is right. DataSource has been correctly updated, before deleteRows I have two element in the array and the data in the tableview start at indexPath = (row: 1, section: 0). Can't find a solution.
Not so convinced, but I've solved in this way:
tableView.performBatchUpdates({
tableView.deleteRows(at: [indexPath], with: .right)
}, completion: { (deleted) in
tableView.reloadData()
self.tableView.isUserInteractionEnabled = true
self.incomingReminders = CoreDataManager.sharedInstance.getIncomingReminders()
if self.incomingReminders.count == 5 {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { (timer) in
self.tableView.beginUpdates()
self.tableView.insertRows(at: [IndexPath(row: self.incomingReminders.count, section: 0)], with: .fade)
self.tableView.endUpdates()
timer.invalidate()
})
}
})
The problem is after deleted a certain number of rows in tableview, it remained dirty. In fact, deleting a bunch of rows, scrolling, and then continuing to delete, crash didn't happen. I needed to reload data. I put
tableView.deleteRows(at: [indexPath], with: .right)
as a batch update. And in completion block I called the reload, with other things. All seems ok for now. Hope this can be useful for someone.
Related
i am trying to use scrollToRow but when call this method tableview does not show any data sometimes. When i check UI inspector i can see table cells but does not seen on screen.
I tried to DispactQueue didn't solve this problem
var forIndex = 0
for item in filteredData {
if let firstCharacter = item.Name?.prefix(1), firstCharacter == char {
let indexpath = IndexPath(row: forIndex, section: 0)
self.tableView.reloadRows(at: [indexpath], with: UITableView.RowAnimation.top)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500), execute: { [weak self] in
self?.tableView.scrollToRow(at: indexpath, at: UITableView.ScrollPosition.top, animated: true)
})
break
}
forIndex += 1
}
char is element of list like a,b,c,d... FilteredData is list of tableview elements
When debugging if scrollToRow method marked breakpoint it's working
Probably the reloadRows (at:) animation is still in progress when you kick of the next causing this strange behavior.
With the breakpoint, the reloadRows (at:) has a chance to finish and on continuing the scrollToRow kicks off after.
Try one of the following if this helps your situation assuming there is no issue with your data source:
1. Change
tableView.reloadRows(at: [indexpath], with: .top)
to
tableView.reloadRows(at: [indexpath], with: .none) so that the UITableView applies a default animation
OR
2. Increase your delay before kicking off the next animation
OR
3. Perform the reload without animation
UIView.performWithoutAnimation { [weak self] in
self?.tableView.reloadRows(at: [indexpath], with: .none)
}
tableView.scrollToRow(at: indexpath,
at: UITableView.ScrollPosition.top,
animated: true)
OR
4. Use beginUpdates and endUpdates which can be useful when performing batch operations on table views
tableView.beginUpdates()
tableView.reloadRows(at: [indexpath], with: .top)
tableView.scrollToRow(at: indexpath,
at: UITableView.ScrollPosition.top,
animated: true)
tableView.endUpdates()
Do any of these give you better results ?
Update
Looking more at the docs for reloadRows(at indexPaths:, here are some lines that stood out for me
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.
So it seems that in some situations animation might not be needed as the cell could be off screen or not needed, so the simplest way to change data is get the cell at the index path and change the data without this function.
Since you are running this in a loop, you most likely start the next indexPath's reloadRows before the previous indexPath's scrollToRow is complete and this can cause some unusual behavior.
Since UITableView does not have it's own completion handler, we can try using CoreAnimation with recursion which can be option 5.
Here is small example I prepared. My sample is like this:
I have 15 rows that are grey in color
I decide that I want to change the color of 4 rows
I store the index of 4 rows in a queue
I will change the color using tableView.reloadRows(at:
After changing the color, I want to scroll to that row using tableView.scrollToRow(at:
Here is how I accomplish that
First I extend the UITableView to use CoreAnimation block to do reloadRows and scrollToRow
extension UITableView
{
func reloadRows(at indexPaths: [IndexPath],
with animation: UITableView.RowAnimation,
completion: (() -> Void)?)
{
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
reloadRows(at: indexPaths, with: animation)
CATransaction.commit()
}
func scrollToRow(at indexPath: IndexPath,
at scrollPosition: UITableView.ScrollPosition,
animated: Bool,
completion: (() -> Void)?)
{
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
CATransaction.setAnimationDuration(2) // set what you want
scrollToRow(at: indexPath, at: scrollPosition, animated: animated)
CATransaction.commit()
}
}
Here is how I use this extension with my view controller set up
class TableViewAnimation: UITableViewController
{
let numberOfRows = 15
// Change the color of these rows in tableview
var colorChangeArray: [Int] = []
// Copy of colorChangeArray used in recursion
var colorChangeQueue: [Int] = []
// Change the color of row to this
private let colorToChange = UIColor.systemBlue
// Normal cell color
private let colorNormal = UIColor.gray
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .white
setUpNavigationBar()
setUpTableView()
}
private func setUpNavigationBar()
{
title = "Table View Animate"
let barButton = UIBarButtonItem(title: "Animate",
style: .plain,
target: self,
action: #selector(didTapAnimateButton))
navigationItem.rightBarButtonItem = barButton
}
private func setUpTableView()
{
tableView.register(CustomCell.self,
forCellReuseIdentifier: CustomCell.identifier)
}
#objc
func didTapAnimateButton()
{
// Queue all the rows that should change
// We will dequeue these in the animation function
// and the recursive function stops when the queue
// is empty
colorChangeQueue = [1, 3, 6, 12]
resumeAnimation()
}
// Recursion function rather than loops using our
// custom functions from extension
private func resumeAnimation()
{
if !colorChangeQueue.isEmpty
{
let rowToChange = colorChangeQueue.removeFirst()
print("starting \(rowToChange) animation")
let indexPath = IndexPath(row: rowToChange,
section: 0)
colorChangeArray.append(rowToChange)
tableView.reloadRows(at: [indexPath],
with: .top) { [weak self] in
self?.tableView.scrollToRow(at: indexPath,
at: .top,
animated: true,
completion: {
// recursively call the function again with a small delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
{
print("complete \(rowToChange) animation")
self?.resumeAnimation()
}
})
}
}
}
}
Finally, here is the data source and delegate but nothing unique is happening here, just adding it for completeness
extension TableViewAnimation
{
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
return numberOfRows
}
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell
= tableView.dequeueReusableCell(withIdentifier: CustomCell.identifier) as! CustomCell
if colorChangeArray.contains(indexPath.row)
{
cell.mainView.backgroundColor = colorToChange
}
else
{
cell.mainView.backgroundColor = colorNormal
}
cell.textLabel?.textAlignment = .center
cell.textLabel?.text = "Row \(indexPath.row)"
return cell
}
override func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 150
}
}
Now it seems like the animations of each of the row is sequenced properly:
And also if you see the print in the console, it is sequenced which did not happen with the loop method:
starting 1 animation
complete 1 animation
starting 3 animation
complete 3 animation
starting 6 animation
complete 6 animation
starting 12 animation
complete 12 animation
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
I have the request to realm database
func allContacts() -> Results<RMContact> {
let realm = self.encryptedRealm()
return realm!.objects(RMContact.self).sorted(byKeyPath: "lastActive", ascending: false)
}
And code from presenter
DispatchQueue.main.async {
let contacts = RMContactsManager.shared.allContacts()
self.notificationToken = contacts.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.view.tableView else { return }
switch changes {
case .initial:
UIView.performWithoutAnimation {
tableView.reloadData()
}
case .update(_, let deletions, let insertions, let modifications):
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), with: .automatic)
tableView.endUpdates()
}
case .error(let error):
fatalError("\(error)")
}
}
}
After setting new lastActive value sequence in tableview didn't change
For the first time sorting is actual for the controller, but after setting new values to lastActive property no changes. Is it an observer problem?
The issue seems to be that you are getting changes in terms of insertions, deletions and modifications.
If you will first do the insertion and then deletion, (This is what you are doing right now), It will insert new values and the datasource will be modified so now when you delete the rows, it will not be correct.
Also in your case, where you are sorting the array, generally two elements will be replaced, one being deleted and other will be added.
So what can solve your problem is, first do deletions and then do insertions. Like this:
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .automatic)
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), with: .automatic)
tableView.endUpdates()
}
I'm developing an iOS Chat App and I have a problem on the chat view. I'm using UITableViewController for the chat view. Sometimes my table jumps when new row is inserted as you can see in video: https://youtu.be/8IgEUJ5uYAc .
This is how I'm inserting and scrolling to the bottom of the table:
self.conversation.append(message)
self.tableView.beginUpdates()
self.tableView.insertRows(at: [IndexPath(row: self.conversation.count - 1, section: 0)], with: UITableViewRowAnimation.none)
self.tableView.endUpdates()
DispatchQueue.main.async {
self.tableView.scrollToRow(at: IndexPath(row: self.conversation.count - 1, section: 0), at: UITableViewScrollPosition.bottom, animated: false)
self.footerView?.isHidden = true
self.theMessage.text = nil
self.switchBottomActions(showSend: false)
}
Each message object has a property called estimatedMessageHeight. I'm saving there the message cell size, so tableView's heightForRowAt code is:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let cellHeight = self.conversation[indexPath.row].estimatedHeight else {
return 0
}
return cellHeight
}
Any solution?
I was also facing same issue and finally I ended up with following solution. It gives me same animation feel like we have in WhatsApp app.
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: [], animations: {
let reloadIndexPath = IndexPath(item: self.arrTableVwData.count - 1, section: 0)
self.tableVw.beginUpdates()
self.tableVw.insertRows(at:[reloadIndexPath], with: UITableViewRowAnimation.fade)
self.tableVw.endUpdates()
self.tableVw.scrollToRow(at: reloadIndexPath, at: .bottom, animated: false)
}, completion: nil)
NOTE: please do not pass any animation types in options:[] as we already passing them in insertRows and scrollToRow methods.
Make this function:
func scrollToBottom(){
DispatchQueue.main.async {
let indexPath = IndexPath(row: self.chatListDB.count-1, section: 0)
self.tblView.scrollToRow(at: indexPath, at: .bottom, animated: false)
}
}
and call it after inserting the value.
You should uncheck Bounce Vertically in the Attribute Inspector for your UITableView.
When i insert row at index path in uitableview, then my tableview scroll to top? Why?
let indexPathForCell = NSIndexPath(forRow: 5, inSection: 1)
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathForCell], withRowAnimation: .Automatic)
tableView.endUpdates()
All code that is invoked during the addition of the cell
func buttonDidPressed(button: CheckMarkView) {
let indexPathForCell = NSIndexPath(forRow: 5, inSection: 1)
buttonPressedTag = button.tag
for checkMark in buttons {
if checkMark.tag == buttonPressedTag {
if buttonPressedTag == 4 {
checkMark.show()
checkMark.userInteractionEnabled = false
cellWithCategories["Recomendation"]?.append("slideCell")
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathForCell], withRowAnimation: .None)
tableView.endUpdates()
}
checkMark.show()
} else {
if (tableView.cellForRowAtIndexPath(indexPathForCell) != nil) {
cellWithCategories["Recomendation"]?.removeLast()
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([indexPathForCell], withRowAnimation: .None)
tableView.endUpdates()
}
checkMark.hide()
checkMark.userInteractionEnabled = true
}
}
}
code for number of rows :
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionKey = keysForSectionTableView[section]
let numberOfRows = cellWithCategories[sectionKey]
return (numberOfRows?.count)!
}
I don't see any code that will make your table view scroll to top.
But you can try change animation to none. If doesn't work then there is must be some other code, thats causing this issue.
let indexPathForCell = NSIndexPath(forRow: 5, inSection: 1)
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathForCell], withRowAnimation: .None)
tableView.endUpdates()