Index out of range when delete cell - ios

Hi and hope you'll help.
In every cell I have a text field which responds directly from table view. I save data via closure in cell.
cell.tableViewClosure = {[unowned self] in
self.tableView.beginUpdates()
// Singleton
Strings.shared.strings[indexPath.row] = cell.textField.text!
self.tableView.reloadRows(at: [indexPath], with: .automatic)
self.tableView.endUpdates()
}
By instance i delete first cell, data array count and number of rows are equal after in log, but if I try to edit text field in last cell and tap return, app crashes with index out of range.
If log during deletion - it is 5 strings in array, but indexPath.row for this proper cell is 5.
But if I reload data in deletion - everything fine with edit last cell but UI is not smooth.

Your problem is that indexPath.row is captured when you assign the closure, for your last cell, it will have the value "4". If you subsequently delete an earlier cell, the last element in your array is now "3", but the captured value doesn't change; it is still 4.
When you then edit the text field in the last cell, the captured value 4 is used to access your model and you get an array bounds exception.
You can use indexPath(for:) to determine the appropriate index path when the closure executes:
cell.tableViewClosure = {[unowned self] in
if let indexPath = self.tableView.indexPath(for: cell) {
self.tableView.beginUpdates()
Strings.shared.strings[indexPath.row] = cell.textField.text!
self.tableView.reloadRows(at: [indexPath], with: .automatic)
self.tableView.endUpdates()
}
}

indexPath.row starts with 0. So the range will be 0-4. If you're using 5 strings in an array and using a .count - that will return a 5.
Just do indexPath.row - 1 to adjust for the off by 1 (index out of range).

Use tableView.indexPath(for: cell) to get the current index

Related

Delete multiple cells from tableView without conflict

I have a tableView where the user can tap on a button inside the cell to delete it. That button is connected with this delegate-function:
extension WishlistViewController: DeleteWishDelegate {
func deleteWish(_ idx: IndexPath){
// remove the wish from the user's currently selected wishlist
wishList.wishes.remove(at: idx.row)
// set the updated data as the data for the table view
theTableView.wishData.remove(at: idx.row)
self.theTableView.tableView.deleteRows(at: [idx], with: .right)
print("deleted")
}
}
Here is how I call the callback (after an animation is finished):
#objc func checkButtonTapped(){
self.successAnimation.isHidden = false
self.successAnimation.play { (completion) in
if completion {
self.deleteWishCallback?()
}
}
}
And this callback is handled in cellForRowAt and passes the indexPath:
cell.deleteWishCallback = {
self.deleteWishDelegate?.deleteWish(indexPath)
}
It works fine until the user clicks multiple buttons right after another as I get a IndexOutOfBounds-Error. What I was thinking of is to store all the incoming indexes in some sort of list and delete them one after another but each index changes as soon as another cell below itself is deleted. What is the best way to get this done?
How are you sending the indexPath to delete in the delegate, can you show code?
You might have retain cycle on your deleted cell, and the index is not valid.
Edit: Solution
You must pass the cell in your closure self.deleteWishCallback?(cell) and then get the actual index path like this
cell.deleteWishCallback = { deletedCell in
let indexPath = tableView.indexPath(for: deletedCell)
wishList.wishes.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .right)
self.deleteWishDelegate?.deleteWish(indexPath)
}
Instead of self.theTableView.tableView.deleteRows(at: [idx], with: .right) just simply reloadData. (self.theTableView.reloadData())
If you set the new datasource for your tableView, you should reload cells.

UITableView Scrolls automatically while textfield begins editing

I am developing an iOS app which has different forms which is populated into a UITableview based on users selection. Each form has different fields like Textfield, DatePicker, PickerView. So I used a single TableViewCell (nib) to hold all these and show or hide the items based on question.
There is save function defined which will save values when user enters to an array.
My issue is, at times my tableview scrolls as if the index goes out of control. like when I select any textfield, Tableview scrolls to top. I have removed all keyboard observer methods and checked, still it is happening.
Below is my save function code:
func saveFormValue(mystr: String) {
//selectedIndex is a global variable (NSIndexPath)
let dict = sections[selectedIndex!.section].questions![selectedIndex!.row]
dict.answer = mystr
sections[selectedIndex!.section].questions![selectedIndex!.row] = dict
let answer = updated_answer.init(ID: ID, form_id: selected_form_id, form_name: formName, section_id: dict.section_id ?? "",question_id: dict.question_id ?? "", question_name: dict.question_name!,question_type:dict.question_type!)
updatedArray.append(answer)
self.tableView.reloadData()
}
This is the code in textfieldDidBeginEditing function (how selectedIndexPath is initialized):
guard let index = tableView.indexPath(for: cell) else {
return
}
selectedIndex = index as NSIndexPath
I have added delegate for cell, and one thing I noticed is, this issue is happening whenever I press pickerview or datepicker once. I couldn't see this issue If I only touch textField cells only.
Please let me know for any further details.
Try this code hope this helps to you.
if let thisIndexPath = tableView.indexPath(for: cell) {
tableView.scrollToRow(at: thisIndexPath, at: .top, animated: false)
}
On textfield delegate method textFieldDidBeginEditing use the following code:
func textFieldDidBeginEditing(_ textField: UITextField) {
let indexParh = NSIndexPath(row: textField.tag, section: 0)
self.constTBL_Bottom.constant = 260
self.tblViewObj.scrollToRow(at: indexParh as IndexPath, at: .middle, animated: false)
}
Also you need to manage the table bottom constant. When you resigning your keyboard set table view constant to 0
Hope this will work :)

Is it unsafe to call reloadData() after getting an indexPath but before removing a cell at that indexPath?

I'm trying to track down a difficult crash in an app.
I have some code which effectively does this:
if let indexPath = self.tableView.indexPath(for: myTableViewCell) {
// .. update some state to show a different view in the cell ..
self.tableView.reloadData()
// show nice fade out of the cell
self.friendRequests.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .fade)
}
The concern is that calling reloadData() somehow makes the indexPath I just retrieved invalid so the app crashes when it tries to delete the cell at that indexPath. Is that possible?
Edit:
The user interaction is this:
User taps a button [Add Friend] inside of table view cell <-- indexPath retrieved here
Change the button to [Added] to show the tap was received. <-- reloadData called here
Fade the cell out after a short delay (0.5s). <-- delete called here with indexPath from #1
I can change my code to not call reloadData and instead just update the view of the cell. Is that advisable? What could happen if I don't do that?
Personally, I'd just reload the button in question with reloadRows(at:with:), rather than the whole table view. Not only is this more efficient, but it will avoid jarring scrolling of the list if you're not already scrolled to the top of the list.
I'd then defer the deleteRows(at:with:) animation by some small fraction of time. I personally think 0.5 seconds is too long because a user may proceed to tap on another row and they can easily get the a row other than what they intended if they're unlucky enough to tap during the wrong time during the animation. You want the delay just long enough so they get positive confirmation on what they tapped on, but not long enough to yield a confusing UX.
Anyway, you end up with something like:
func didTapAddButton(in cell: FriendCell) {
guard let indexPath = tableView.indexPath(for: cell), friendsToAdd[indexPath.row].state == .notAdded else {
return
}
// change the button
friendsToAdd[indexPath.row].state = .added
tableView.reloadRows(at: [indexPath], with: .none)
// save reference to friend that you added
let addedFriend = friendsToAdd[indexPath.row]
// later, animate the removal of that row
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if let row = self.friendsToAdd.index(where: { $0 === addedFriend }) {
self.friendsToAdd.remove(at: row)
self.tableView.deleteRows(at: [IndexPath(row: row, section: 0)], with: .fade)
}
}
}
(Note, I used === because I was using a reference type. I'd use == with a value type that conforms to Equatable if dealing with value types. But those are implementation details not relevant to your larger question.)
That yields:
Yes, probably what's happening is the table view is invalidating stored index path.
To test whether or not it is the issue try to change data that is represented in the table right before reloadData() is called.
If it is a problem, then you'll need to use an identifier of an object represented by the table cell instead of index path. Modified code will look like this:
func objectIdentifer(at: IndexPath) -> Identifier? {
...
}
func indexPathForObject(_ identifier: Identifier) -> IndexPath? {
...
}
if
let path = self.tableView.indexPath(for: myTableViewCell)
let identifier = objectIdentifier(at: path) {
...
self.tableView.reloadData()
...
if let newPath = indexPathForObject(identifier) {
self.friendRequests.removeObject(identifier)
self.tableView.deleteRows(at: [newPath], with: .fade)
}
}

UITableView deleterows scroll to top

I made a query to delete specified rows in UITableView but the problem when the deleteRows is called the UITableView scroll to top so how to prevent it scrolling to top? and why it scroll to top!
I try called a method to force it scroll to down but the tableview scroll to top first after return
the code of remove
FIRDatabase.database().reference().child("messages").child(self.roomId).observe(.childRemoved, with: {(snap) in
print("child Removed")
for msg in self.messageArray {
let message: NSDictionary = msg as! NSDictionary
let messageContent: [String: Any] = message as! [String: Any]
if messageContent["messageID"] as! String == snap.key {
let indexRemoved = self.messageArray.index(of: msg)
self.messageArray.remove(msg)
self.userArray.removeObject(at: indexRemoved)
let indexPath = IndexPath(row: indexRemoved, section: 0)
print(indexPath)
self.conversationTableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.none)
break;
}
}
})
Please be sure you are not using table view method like 'estimatedHeightFor...' to calculate hight to scroll table view after row delete. One more point to be sure is, reloadData method should not use when deleting row from table view if you don't want to table scrolling.
If all are good then on deleting a particular row, table view will move all rows up to fill deleted row empty space.
There are a few issues with the code that can be easily corrected.
First before doing anything, grab the current scroll position of the first visible item (given the cells are the same size)
let savedIndex = myTableView.indexPathsForVisibleRows?.first
then once the row is deleted from the array and you reload the tableview, you can return to where it was with
myTableView.scrollToRowAtIndexPath(savedIndex, atScrollPosition: .Top, animated: false)
If you need another option (variable heights), you can use
tableView.contentOffset.y;
Also, there's no need to iterate over the entire array to find the row to delete from it. Assuming the Firebase nodes(s) data are stored in the array as a class, in the array with the Firebase key as a property of each class, you can simply get the index of that element in the array and remove it. Here's an option
let key = snapshot.key
if let index = self.myArray.indexOf({$0.firebaseKey == key}) {
self.myArray.removeAtIndex(index)
self.myTableView.reloadData()
}
This object would look like:
class MyClass {
var firebaseKey: String?
var some_var: String?
var another_var: String?
}

How do I programmatically delete row 0 of UITableView?

I have an app that has a table view. Every minute or so I want the first row of the UITableView to disappear. It would be nice to have it animate off. I am assuming I need to use something like this:
let myNSIndexPath = [NSIndexPath(forRow: 0, inSection: 0)]
mainTableView.deleteRowsAtIndexPaths(myNSIndexPath, withRowAnimation:UITableViewRowAnimation.Fade)
This doesn't work. I'm not sure exactly how to use IndexPath correctly. My table only has one section and I always want the cell at Index 0 to be removed. What must to do to get this working correctly?
One way
Use removeAtIndex
yourarray.removeAtIndex(indexPath.row)
And don't forget to reload the table view
tableView.reloadData()
Second Way
yourIndexPath = [NSIndexPath(forRow: 0, inSection: 0)]
yourtable.beginUpdates() //if you are performing more than one operation use this
yourarray.removeObjectAtIndex(myNSIndexPath.row)
yourtable.deleteRowsAtIndexPaths(NSArray(object: yourIndexPath), withRowAnimation: UITableViewRowAnimation.Left)
yourtable.endUpdates() //if you are performing more than one operation use this
Took help from
#Rmaddy
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableView_Class/
You can delete the first cell like this.
One way
let myNSIndexPath = [NSIndexPath(forRow: 0, inSection: 0)]
self.tableView.beginUpdates()
self.arrayData.removeObjectAtIndex(myNSIndexPath.row) // also remove an array object if exists.
self.tableView.deleteRowsAtIndexPaths(NSArray(object: myNSIndexPath), withRowAnimation: UITableViewRowAnimation.Left)
self.tableView.endUpdates()
Second way
yourArr.removeAtIndex(0)
self.tableView.reloadData()
in SWIFT 4
You can delete the row like this.
let indexPathRow=sender.tag
let indexPath = IndexPath(row: indexPathRow, section: 0)
arrayList.remove(at: indexPathRow)
mainTableView.deleteRows(at: [indexPath], with: .none)

Resources