UITableView - Destructive UIContextualAction does not reload data - ios

I'm trying to use the iOS 11 way to add swipe actions in a table view row. I want to add an action to delete a row.
My test table view displays numbers from 0 to 9, the data source is a simple array of integers, called numbers:
class ViewController: UIViewController {
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}
When I select a row, I print the associated value of numbers:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(numbers[indexPath.row])
tableView.deselectRow(at: indexPath, animated: true)
}
I implement the new delegate trailingSwipeActionsConfigurationForRowAt as below:
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let delete = UIContextualAction(style: .destructive, title: "Delete") { (_, _, completionHandler) in
self.numbers.remove(at: indexPath.row)
completionHandler(true)
}
return UISwipeActionsConfiguration(actions: [delete])
}
When I swipe on a row, the deletion works fine. But, and that's my problem, when I select another row after the deletion, the associated IndexPath is wrong...
Example:
I select the first row, the value printed is 0: OK.
I delete the first row.
I select the new first row, the value printed is 2, because the IndexPath passed in parameter is row 1, instead of row 0: not OK.
What do I do the wrong way?

It is important to remember that there's a model, the numbers array in your case, and a view, the cells that are displayed. When you remove the element from numbers you are only updating the model.
In most cases, as you have it coded, you would get an error shortly thereafter because the model is out of sync with the view.
However, my testing indicates that when the style is .destructive and you pass true to completionHandler Apple is sort of removing the row for you. Which is why you are seeing the row disappear. But there's something not quite right about it and I can't find any documentation on it.
In the meantime, just do this:
self.numbers.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completionHandler(true)
And always remember that if you change with the model you need to update the view.

It is very confusing that calling the completion handler with true updates the view by removing the cell, but does not update the table view's notion of which which remaining cells are at which index path.
The documentation around this is very scant, so I ran a test and found that calling the completion handler with true FIRST then calling deleteRows results in the "most correct" animation.
Batch updates with completion called first:
tableView.beginUpdates()
completion(succeeded)
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
Batch updates with completion called second:
tableView.beginUpdates()
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(succeeded)
tableView.endUpdates()
Completion called first:
completion(succeeded)
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
Completion called second
users.remove(at: indexToRemove)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(succeeded)
Image mid-animation:
The right panels both have the remaining cell partially covered by the cell that is being removed, while the bottom left panel introduces a white view from somewhere.

Related

How do I move cells according to a sort when a cell is clicked?

I am look for a way to move a cell when tapped to its place according to a sort.
I have Googled and not found much and personally have never done something like this before.
This is my sort
let sort = NSSortDescriptor(key: #keyPath(Item.name), ascending: true)
This is my didSelectRow
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = fetchedRC.object(at:indexPath)
item.isComplete.toggle()
do {
try context.save()
//tableView.deselectRow(at: [indexPath.row], animated: true)
//tableView.reloadRows(at: [indexPath], with: .automatic)
} catch let error as NSError {
print("Could not save. \(error.localizedDescription)")
}
}
I would like for selected cells to be crossed out (I have handled already) and moved to the bottom of the tableView which is what my sort is doing. The sort only works when loaded, as expected, but I would like to move the cell that gets selected to its respective place after the initial appearance of the table.
EDIT:
I decided to test the sort and it was set to my names instead of my isComplete field so it now moves them accordingly but now it does not cross out the text like it is supposed to do.
I finally found this solution and it seems to work how intended. All I needed to do was replace my exciting .move to this.
case .move:
if indexPath != nil {
self.tableView.deleteRows(at: [currentIndexPath!], with: .automatic)
}
if let newIndexPath = newIndexPath {
self.tableView.insertRows(at: [newIndexPath], with: .automatic)
}

ios application crashes when deleting line in uitableView

I have a problem in my UITableView which filled with data: [(gamme: String, [(product: String, quantity: Double)])], everything works fine: inserting rows and sections, deleting row and section, reloading.
But sometimes and when I try to delete lots of lines in fast way (line by line by swiping the line the table and tap (-) ). it leads to a crash like in the screenshot.
The issue is hard to reproduce in development app. but my clients still reports it. My clients are professionals (not normal users) and are expected to use the in a fast way with medium to large data.
and this is my func that delete lines:
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .destructive, title: "-") { (action, indexPath) in
let cmd = self.groupedData[indexPath.section].1.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .right)
self.delegate?.didDeleteCmdLine(cmd)
if self.groupedData[indexPath.section].1.count == 0 {
self.groupedData.remove(at: indexPath.section)
tableView.deleteSections(IndexSet(integer: indexPath.section), with: UITableViewRowAnimation.right)
}
}
return [delete]
}
why is that happening ?
This is a screen of xcode organiser for the crash
Edit:
Checking if groupedData is accessed by any thread other than main proposed by #Reinhard:
private var xgroupedData = [(gamme: GammePrdCnsPrcpl, [cmdline])]()
private var groupedData: [(gamme: GammePrdCnsPrcpl, [cmdline])] {
get {
if !Thread.isMainThread {
fatalError("getting from not from main")
}
return xgroupedData
}
set {
if !Thread.isMainThread {
fatalError("setting from not from main")
}
xgroupedData = newValue
}
}
but the groupedData variable is accessed only from main thread
tableView.beginUpdates()
self.tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
Changes in João Luiz Fernandes answer....try this
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
objects.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
reference (Hacking with swift . https://www.hackingwithswift.com/example-code/uikit/how-to-swipe-to-delete-uitableviewcells )
u can try this
var myDataArray = ["one","two","three"]
//wanna insert a row then in button action or any action write
myDataArray.append("four")
self.tblView.reloadData()
// wanna delete a row
myDataArray.removeObject("two")
// you can remove data at any specific index liek
//myDataArray.remove(at: 2)
self.tblView.reloadData()
try to use like this function.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
print("Deleted")
// do your delete your entires here..
//
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
After the user tapped the delete button, you remove the corresponding row and (if this was the last row of that section) the corresponding section from your data source groupedData, which is an array. However, array operations are not thread-safe.
Can it be that another thread is using this array while it is modified by the delete action?
In this case, the app can crash. The danger is of course higher, when several actions are triggered in a short time, which seems to be the case as you described it.
One way (maybe not the best) to avoid multithreading problems is to access the array only on the main thread.
If this slows down the main thread, one can use a synchronised array that allows multiple reads concurrently, but only a single write that blocks out all reads, see here.
There is just a update #codeByThey's answer. Please Update your DataSource file as you delete that particular row.
tableView.beginUpdates()
self.whatEverDataSource.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
This will result that your DataSource is also update same time as the TableView. The crash is happening may be due to the DataSource is not updated.
Can you try removing the section at once instead of trying to remove the row once, and then the section which it belongs to, when the last item in a section is being removed?
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .destructive, title: "-") { [weak self] (action, indexPath) in
// Use a weak reference to self to avoid any reference cycles.
guard let weakSelf = self else {
return
}
tableView.beginUpdates()
let cmd: cmdline
if weakSelf.groupedData[indexPath.section].1.count > 1 {
cmd = weakSelf.groupedData[indexPath.section].1.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .right)
} else {
// Remove the section when the `items` are empty or when the last item is to be removed.
cmd = weakSelf.groupedData.remove(at: indexPath.section).1[indexPath.row]
tableView.deleteSections([indexPath.section], with: .right)
}
weakSelf.delegate?.didDeleteCmdLine(cmd)
tableView.endUpdates()
}
return [delete]
}

iOS Collapse/ Expand Sections by clicking another section in TableView

I have a question about making a table view section expand/collapse. There are many pieces of information on the web about this topic, but my question is a little bit different.
My question is, how can I make an additional section when the user taps on another section?
The following images may help you understand my situation.
At the top of the table view we have one small section (section 0).
If I tap it...
Section 1 and 2 should be created between section 0 and 3.
How can I do this? Is there any way or code? Please help me.
Also, I tried this code but got a sigabrt error :(
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView.beginUpdates()
let indexSet = NSMutableIndexSet()
indexSet.add(indexPath.section + 1)
indexSet.add(indexPath.section + 2)
if indexPath.section == 0 {
if expandCol == true {
self.tableView.deleteSections(NSIndexSet(index: 1) as IndexSet, with: .automatic)
self.tableView.deleteSections(NSIndexSet(index: 2) as IndexSet, with: .automatic)
expandCol = !expandCol
} else {
self.tableView.insertSections(NSIndexSet(index: 1) as IndexSet, with: .automatic)
self.tableView.insertSections(NSIndexSet(index: 2) as IndexSet, with: .automatic)
expandCol = !expandCol
}
}
self.tableView.endUpdates()
self.tableView.reloadData()
}
You can keep a flag variable on view-controller for selectedSection.
var selectedSection: Int = -1
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == selectedSection ? <section-row-count> : 0
}
Set the flag value in section header action method:
func actionSectionHeader(tag: Int) {
selectedSection = tag
self.tableView.reloadData()
}
To add a new section into UITableView, you need
1 - Update your data. For example, insert your sections in a data array, or change their visibility flags.
2 - Call the following method.
tableView.insertSections([1, 2], with: .automatic)
or
tableView.reloadData()
The first method will update your UITableView with an animation. The second one will update it instantly. For better UX I'd recommend you to use the first one.
Update on your code
At first, you can pass all sections indices at once using a simple array. Like that
if expandCol {
self.tableView.deleteSections([1, 2], with: .automatic)
} else {
self.tableView.insertSections([1, 2], with: .automatic)
}
expandCol = !expandCol
The second problem is that you're calling reloadData after you call the animated update of your table.
And the most important, you need to change the data. You UITableView has a dataSource and it returns a number of sections and cells. After user interacts with your trigger cell, you need to make changes at that data before calling the update.

Swift: Custom section header includes buttons to either delete rows or add a row for ONLY that section's rows

I'm new to Swift, so please elaborate on your answer.
Basically I've created a custom header using a nib and created a subclass of UITableViewHeaderFooterView associating to the nib I created. The custom header contains two buttons which one is "Add Object" and when the user clicks on it, it should just insert a custom cell (Yes. My cells are custom cells that dynamically display when the application launches) row with some default data and the other is "Delete Object" which enables the edit mode and allows the user to delete rows. It also contains a label to display the title of that section header.
I don't want to use navigation control or create my buttons programmatically, so I'm just mentioning it before anyone gives me an answer like that. I will explain exactly what I've done and what I'm trying to do.
The problem I am having is whenever I click "Delete Object" it does enable the edit mode and displays the red circles, but to ALL sections. I only want it to show for the section I clicked it on. I've already tried to use canEditRowAt and it doesn't seem to work. The problem I keep getting is it either displays the red circles to just one section regardless which section I click the "Delete Object".
func tableView(_ tableView: UITableView, commit editingStyle:
UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
//There are two sections
switch indexPath.section {
case 0:
if editingStyle == .delete{
//Updating data model before removing
firstArray.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .left)
}
else if editingStyle == .insert{
firstArray.append(objects(name: "Test", second: "Cell", image: UIImage(named: "Unknown")!))
let indexPath = IndexPath(row: firstArray.count - 1, section: 0)
tableView.insertRows(at: [indexPath], with: .left)
}
case 1:
if editingStyle == .delete{
//Updating data model before removing
secondArray.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .left)
}
else if editingStyle == .insert{
let indexPath = IndexPath(row: secondArray.count - 1, section: 0)
tableView.insertRows(at: [indexPath], with: .left)
}
default:
break
}
}
For the buttons inside the nib, I did something similarly to how you would unwind from a viewController. I created the #IBAction outlets in the firstViewController (which is the datasource and delegate of the TableView) and inside the nib I control + clicked on each button and dragged the line to the firstResponder box and assigned it to the events I created. I've tested them and they work.
#IBAction func deleteTheObject(sender: UIButton){
//This does show the edit mode and changes the buttons name each time
//it's clicked, but I only want it to enable the edit mode on the
// section that the section's header "Delete Object" button was clicked on.
//NOT both sections
if self.table.isEditing == true{
table.setEditing(!table.isEditing, animated: true)
sender.setTitle("Done", for: .normal)
}
else{
self.table.isEditing = true
sender.setTitle("Delete Object", for: .normal)
}
}
#IBAction func addTheObject(sender: UIButton){
//Should add row to section
}
Add this to only allow, for example section1, editable
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
if indexPath.section == 1 {
return true
}
return false
}

How to remove a cell from tableView with animation through IBAction? iOS Swift 3.0

I'm building a todo-list app, and I'm holding a task model array and tableView in my view controller.
Each cell in this tableView contains several UI elements, one of them is a UIView that is actually a checkbox, implemented by Skyscanner's pod:
https://github.com/Marxon13/M13Checkbox
I would like to remove a cell when a user tap on the checkbox (with animation).
I set an IBAction to the UIView, I know which element in the array I want to remove (I tagged each UIView) but I cannot use the method
tableView.deleteRows(at: [IndexPath], with: UITableViewRowAnimation)
since I don't have the index path.
I want to find a nice way to remove the cell with an index, preferably without holding an indexPath variable (I tried that but not sure how to implement it correctly since cells can be removed from various indexes).
Thanks for your help!
Thanks for everyone that helped!
I'm answering my own question because it was a combination of your answers!
as Puneet Sharma, Reinier Melian and Yun CHEN said, I managed to create an index path inside the function.
Puneet's comment is very important, you must remove the element from the array before you remove the cell.
PS.
Maybe I could create the IndexPath way before I even asked the question, but in the line
self.tableView.deleteRows(at: [IndexPath(row: checkbox.tag, section: 0)], with: .automatic)
Xcode does not auto complete this initialization at all.
This is the code that remove the cell just like I wanted:
#IBAction func completeBtnPressed(_ sender: Any) {
let checkbox = (sender as! M13Checkbox)
self.tasks.remove(at: checkbox.tag)
self.tableView.deleteRows(at: [IndexPath(row: checkbox.tag, section: 0)], with: .automatic)
}
Thanks a lot!
Set the action event on the checkbox/Button like bellow in cellForRowatIndexPath. don't forgot to add the tag on every checkbox/Button.
cell.btnCounter.tag = indexPath.row
cell.btnCounter.addTarget(self, action: #selector(self.buttonClicked), for: .touchUpInside)
You need to handle event as below
func buttonClicked(_ sender: UIButton) {
//Here sender.tag will give you the tapped checkbox/Button index from the cell
your_array.remove(at: sender.tag) //Remove your element from array
tableView.deleteRows(at: [IndexPath(row: sender.tag, section: 0))], with: .automatic) //Hope it is in section 0
}
Hope this help you
// Check this snap code
#IBAction func btnAction(_ sender : UIButton){
// get Indexpath with Button position
let buttonPosition = sender.convert(CGPoint.zero, to: self.tableview)
let indexPath: IndexPath? = tableview.indexPathForRow(at: buttonPosition)
self.yourArrayDatasource.remove(at: indexPath.row) // don't forget to replace your array of data source
self.tableview.deleteRows(at: [indexPath!], with: UITableViewRowAnimation.automatic)
}
I hope it help you

Resources