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]
}
Related
I have a post removal function that works great:
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath){
posts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { let more = UITableViewRowAction(style: .default, title: "Report") { action, index in self.posts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade) }; more.backgroundColor = UIColor.blue; return [more] }
But when you hit refresh, it reloads the posts that my user reports. I want the refresh action not to show these removed posts. However, I want every other user to still see these posts and make their own judgement, so I do not want these posts removed from the database.
What would you advise doing to the refresh function?
For reference, here is my handleRefresh function:
#objc func handleRefresh() {
print("Refresh!")
toggleSeeNewPostsButton(hidden: true)
newPostQuery.queryLimited(toFirst: 20).observeSingleEvent(of: .value, with: { snapshot in
var tempPosts = [Post]()
let firstPost = self.posts.first
for child in snapshot.children {
if let childSnapshot = child as? DataSnapshot,
let data = childSnapshot.value as? [String:Any],
let post = Post.parse(childSnapshot.key, data: data),
childSnapshot.key != firstPost?.id {
tempPosts.insert(post, at: 0)
}
}
self.posts.insert(contentsOf: tempPosts, at: 0)
let newIndexPaths = (0..<tempPosts.count).map { i in
return IndexPath(row: i, section: 0)
}
self.refreshControl.endRefreshing()
self.tableView.insertRows(at: newIndexPaths, with: .top)
self.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
self.listenForNewPosts()
// return completion(tempPosts)
})
}
I am not sure where you are storing the data and on refresh if you are fetching the details via Api or local database. But from your I am sure that you need to maintain a flag value in server database or local database at post level that will indicate if this post is to be shown to the tableview or not. If there are multiple users who should not see this post then you have manage an array of the UserIds who are not supposed to see this post at post level entry and when you fetch the details in the application, you can filter the details and show those in the tableview.
I have a dozen table view controllers that all work as expected, then I have this one which crashes with:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
The code doing this has been modified several times with different options but to no effect.
import UIKit
import CoreData
class QTypeVCY: UITableViewController, NSFetchedResultsControllerDelegate
{
let app = UIApplication.shared.delegate as! AppDelegate
override func viewDidLoad()
{
super.viewDidLoad()
}
override func numberOfSections(in tableView: UITableView) -> Int
{
let sections = frc.sections
return sections!.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
guard let sections = self.frc.sections else
{
fatalError("No sections in fetchedResultsController")
}
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "QQQQ", for: indexPath)
let qtype = frc.object(at: indexPath)
cell.textLabel?.text = qtype.title
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete
{
do
{
let qtype = frc.object(at: indexPath)
let context = self.frc.managedObjectContext
context.delete(qtype)
try context.save()
tableView.deleteRows(at: [indexPath], with: .fade)
}
catch
{
debugPrint(error)
}
} 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
}
}
lazy var frc: NSFetchedResultsController<Qtype> =
{
let context = self.app.persistentContainer.viewContext
let req: NSFetchRequest<Qtype> = Qtype.fetchRequest()
req.fetchBatchSize = 10
let sortDescriptor1 = NSSortDescriptor(key: #keyPath(Qtype.specialty), ascending:true)
let sortDescriptor2 = NSSortDescriptor(key: #keyPath(Qtype.title), ascending:true)
req.sortDescriptors = [sortDescriptor1, sortDescriptor2]
let afrc = NSFetchedResultsController(fetchRequest: req, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
afrc.delegate = self
do
{
try afrc.performFetch()
}
catch
{
print(error.localizedDescription)
fatalError("Abort while fetching Qtype")
}
return afrc
}()
}
The crash occurs on the tableview.deleteRows statement. I have tried surrounding the code with beginUpdate/endUpdates, with and without the performFetch, even tried re-entering the code in case I had a typo that I missed. This same basic code is working fine on other tables/view controllers, just this one.
The entity is just made up of strings. I had it with and without relationships to other entities.
The row is deleted since the next time I run the app it is missing. Also, one other thing about this table is that calling reloadData after adding a new object does not add the row. I need to leave the tableview and reenter it. I'm sure the two are related but can't say why.
Since originally posting this, I included the entire VC code instead of just the offending code. I have also tried swapping the Entity with a different entity where this issue does not occur, but the program still crashes even with a different entity.
You forgot to remove the item also from the data source array
let qtype = qtypes[indexPath.row]
let context = self.frc.managedObjectContext
context.delete(qtype)
try context.save()
qtypes.remove(at: indexPath.row) // <--
tableView.deleteRows(at: [indexPath], with: .fade)
I wish I could offer the actual solution, but the above code now works. I had updated XCode to the 1/24/18 version but I don't think that is the solution. I also stripped out a few things I had in the original code that crashed, but then I put them back and it still works. I suspect I had a typo somewhere, but after spending a half hour trying to locate it, I'm declaring victory and moving on.
I just solloved a problem similar to yours.
when deleted data in CoreData, TableView data array also delete before appDelegate.saveContext()
Code in func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
self.tableviewArrayData.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
appDelegate.saveContext()
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
}
I have a table that uses a coredata fetch to populate its self, the fetch controller stores the delete row action.
However I want to add an edit feature to the row as well, I dont seem to be able to add this along side the delete feature I currently have and need to use editActionsForRowAt to get it to appear.
When i use editActionsForRowAt it overrides by existing Fetch Controllers row actions and causes the delete to no longer work.
Any idea how I can add just an edit next to the existing delete without over writing or messing with the fetch results controller?
Here is the code for the fetch controller that is the source for the table and its delete case and delete func, I want to ideally add an edit button to this, without having to overwrite it all with editActionsForRowAt
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch (type) {
case .insert:
if let indexPath = newIndexPath {
workoutDesignerTable.insertRows(at: [indexPath], with: .fade)
}
break;
case .delete:
if let indexPath = indexPath {
workoutDesignerTable.deleteRows(at: [indexPath], with: .fade)
}
break;
case .update:
if let indexPath = indexPath, let cell = workoutDesignerTable.cellForRow(at: indexPath) as? RoutineTableViewCell {
configure(cell, at: indexPath)
}
break;
default:
print("...")
}
}
// MARK: - DELETING TABLE ROW
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let UserExercise = fetchedResultsController.managedObjectContext
UserExercise.delete(self.fetchedResultsController.object(at: indexPath))
do {
try UserExercise.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
For more information please find the below screenshot of current Table Row Edit Actions.
From the documentation tableView(_:editActionsForRowAt:)
Use this method when you want to provide custom actions for one of your table rows. When the user swipes horizontally in a row, the table view moves the row content aside to reveal your actions. Tapping one of the action buttons executes the handler block stored with the action object.
If you do not implement this method, the table view displays the standard accessory buttons when the user swipes the row.
So you need to also pass the delete action button with your other buttons.
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let btnEdit = UITableViewRowAction(style: .default, title: "Edit") { action, indexPath in
//Put the code of edit action
}
let btnDelete = UITableViewRowAction(style: .destructive, title: "Delete") { action, indexPath in
//Put the code of delete action
}
return [btnEdit, btnDelete]
}
Note: If you are implementing tableView(_:editActionsForRowAt:) then there is no need to implement tableView(_:commit:forRowAt:) method.
i'm trying to delete a cell from UITableView in swift, i follow this tutorial: http://www.ioscreator.com/tutorials/delete-rows-table-view-ios8-swift
the problem is my UITableView has many section, so i can't delete the cell the way like the tutorial.
any one know how to delete cell form table with multiple section?
thanks.
You cannot delete multiple cells at once with the method described in the tutorial. That will only work for single cell. If you select multiple cells and use button, for example, to trigger delete action, your code could look something like this:
if let indexPaths = tableView.indexPathsForSelectedRows() as? [NSIndexPath] {
for indexPath in indexPaths {
// one by one remove items from your datasource
}
tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
}
Instead of using numbers[row] in the example you can use numbers[section][row]. So the code will look like:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numbers[section].count
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
numbers[indexPath.section].removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
Neither of answers worked for me. Swift Array indexes are updated upon removal hence for-in loop for indexes from .indexPathsForSelectedRows() provided unexpected results i.e. wrong data/tables removed and eventually crash with index outside of array bounds error. Found good (but really outdated) Objective-C iOS Developer Library example. But it utilised NSMutableArray removeObjectsAtIndexes method, not present with Swift Array. Anyway a good deal of useful tricks in there so worth take a look.
The method which work for me is part from that example but instead of removeObjectsAtIndexes do-while is used to remove rows one by one until all selected rows are removed. The method below called by UIAlertAction similar to Apple example.
func deleteSelectedRows() {
// Unwrap indexPaths to check if rows are selected
if let _ = tableView.indexPathsForSelectedRows() {
// Do while all selected rows are deleted
do {
if let indexPath = tableView.indexPathForSelectedRow(){
//remove from table view data source and table view
self.dataSource.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
} while tableView.indexPathsForSelectedRows() != nil
}else{
// Delete everything, delete the objects from data model.
self.dataSource.removeAll(keepCapacity: false)
// Tell the tableView that we deleted the objects.
// Because we are deleting all the rows, just reload the current table section
self.tableView.reloadSections(NSIndexSet(index: 0), withRowAnimation: .Automatic)
}
// Exit editing mode after the deletion.
self.tableView.setEditing(false, animated: true)
}
Edit: While do-while did it trick for small example I've been working with (jus starting with swift) It's not efficient. Either extending Array or make data source Equatable and use find() or .filter is preferable.But I'm sure there should be a simpler way. The one I'm using now is described on link below:
http://www.rockhoppertech.com/blog/swift-remove-array-item/
func == (lhs: myDataSource, rhs: myDataSource) -> Bool {
if lhs.data == rhs.data &&
lhs.otherData == rhs.otherData {
return true
}
return false
}
struct myDataSource: Equatable {
let data: String
let otherData: String
}
And then:
if let selectedRows = tableView.indexPathsForSelectedRows(){
var objectsToDelete = [myDataSource]()
for selectedRow in selectedRows {
objectsToDelete.append(myDataSource[selectedRow.row])
}
for object in objectsToDelete {
if let index = find(myDataSource, object){
myDataSource.removeAtIndex(index)
}
}
}
tableView.deleteRowsAtIndexPaths([selectedRows], withRowAnimation: .Automatic)
try to this. this works fine.
But don't forget to this before.
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool
{
return true
}
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath)
{
if editingStyle == .Delete
{
arrayOfnames.removeAtIndex(indexPath.row)
self.tableViewww.reloadData()
}
}