Adding Edit Row Action to TableView that uses Fetch Controller? - ios

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.

Related

UITableView Destructive Context Menu Animation

I'm adding iOS13 context menus to my table view. One of the menu actions allows the user to delete the item:
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions in
let deleteAction = UIAction(title: "Delete", image: UIImage(systemName: "trash.fill"), identifier: nil, discoverabilityTitle: "", attributes: UIMenuElement.Attributes.destructive) { action in
self.data.remove(at: indexPath.row)
//Remove from the table.
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
return UIMenu(title: "", children: [deleteAction])
}
}
I'm using the default preview view controller (so it just shows the cell). I'm currently seeing a weird animation artifact where the context menu preview is displayed while the items below the row being removed animated up, then the preview fades away to white (so it looks like there is a blank row in the list), then the table repaints and displays the item that was covered up.
This is using the default cell but it looks a lot worse when using a customized cell with a lot more information. Is there anyway to make this action animate better?
I ran into this problem as well. The bug is probably due to the fact that the original cell used to generate the preview was deleted, moved or changed.
The solution I found was to implement the delegate method tableView(_:previewForHighlightingContextMenuWithConfiguration:), passing it the original cell as the view, but customising UIPreviewParameters to use UIColor.clear:
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
guard let indexPath = configuration.identifier as? IndexPath, let cell = tableView.cellForRow(at: indexPath) else {
return nil
}
let parameters = UIPreviewParameters()
parameters.backgroundColor = .clear
return UITargetedPreview(view: cell, parameters: parameters)
}
In order to identify the original cell in this delegate method, you'd need a way to identify it. One way is to set the indexPath as the identifier to UIContextMenuConfiguration, like:
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { _ in
return UIMenu(title: "", children: [
UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { action in
self.data.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
])
}
}
However if your data can change between the presentation of the context menu and the action, then you need a more robust way to identify it.
I did not have to implement tableView(_:previewForDismissingContextMenuWithConfiguration:) for this to work.

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]
}

CustomCell content not updated after fetchRequest with changed CoreData values

I must be missing something simple but cannot identify what it is. BASIC and FORTRAN sure seemed a lot easier... just not as cool as this stuff is.
App is to display & sort flashcards stored in CoreData based on a predicate. After selecting options which modify the predicate, a UITableView is displayed listing the cards meeting the criteria in abbreviated form. Selecting a card row segues to a 2nd view controller with identical predicate setup in UITableView only more content shown & "Skill Level" can be modified and saved via CoreData on this 2nd view controller. Depending on the fetchRequest predicate, a card may no longer match the criteria and might be excluded from a subsequent fetchRequest. This all seems to function properly.
Selecting "Back" returns to the 1st UITableView controller from the 2nd, via an unwind segue. At this point, the fetchRequest is again executed along with a tableview.reloadData(). On return, flashcards sort in the proper order based on any changes. Flashcards that no longer meet the predicate criteria are properly excluded from the table.
The problem is this - the custom cell contents do not reflect any changes made (stars graphic doesn't change and numeric value of the Skill Level doesn't change when printed from the tableView). TableView sorts correctly and doesn't include items it shouldn't which seems to indicate the updated information was saved and has somehow been taken into consideration by the tableView. TableView just doesn't show it on the screen.
If I select "Back" again, returning to the "Home" or initial viewController & immediately segue to it again the tableView displays properly.
I've tried placing tableView.reloadData() in various locations(with and without .self), moving the fetch & reloads in and out of DispatchQueue.main.async. Tried replacing the control in the custom cell with images but that didn't work either. Can't seem to find anything quite like it on stack overflow or elsewhere on internet.
Any assistance would be greatly appreciated.
Custom Cell:
import UIKit
class CardsTableViewCell: UITableViewCell {
// MARK: Properties
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var difficultyImageView: UIImageView!
#IBOutlet weak var knowItControl: KnowItControl!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
UnwindFromSegue:
#IBAction func unwindToCardsTableViewController(sender: UIStoryboardSegue) {
DispatchQueue.main.async{
// RELOAD THE FLASHCARDS FOR TABLEVIEW
do {
try self.fetchedResultsController.performFetch()
} catch let error as NSError {
print("Fetching error: \(error), \(error.userInfo)")
}
print("\n UNWIND - About to reload table")
self.tableView.reloadData()
}
print("UNWIND SEGUE from DetailedCardsView")
// Considered giving up and just dumping back to Main Screen.
// dismiss(animated: true, completion: nil)
}
Assembling the tableView using extensions in the tableViewController
// MARK: - ConfigureCardsTableViewCell
extension CardsTableViewController {
func configure(cell: UITableViewCell, for indexPath: IndexPath) {
guard let cell = cell as? CardsTableViewCell else { return }
let flashCards = fetchedResultsController.object(at: indexPath)
cell.knowItControl.rating = Int(flashCards.cardKnowIt)
cell.questionLabel.text = flashCards.cardQuestion
// * * * ASSIGN THE IMAGE BASED ON THE levelDifficulty * * *
switch flashCards.levelDifficulty {
case "e"?:
cell.difficultyImageView.image = UIImage(named: "activeLetterE")
case "g"?:
cell.difficultyImageView.image = UIImage(named: "activeLetterG")
case "m"?:
cell.difficultyImageView.image = UIImage(named: "activeLetterM")
default:
cell.difficultyImageView.image = UIImage(named: "activeLetterE")
}
// ************************* END OF ASSIGN IMAGE ****************************************
print("assembling cell \(indexPath.row) *** knowIt - \(Int(flashCards.cardKnowIt))") // Observe what is going on, see if value was updated (it's not).
}
}
// * * * * T A B L E V I E W D A T A S O U R C E * * * *
// MARK: - UITableViewDataSource
extension CardsTableViewController: UITableViewDataSource {
// * * * C O N F I G U R E T H E C E L L V I A A C A L L * * *
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cardCellReuse", for: indexPath)
configure(cell: cell, for: indexPath)
print(" Configuring cell \(indexPath)") // Observe what is going on.
return cell
}
// general tableView functions for number of rows, sections, etc.
}
The viewController also contains the generic NSFetchedResultsControllerDelegate which I believe accurately matches all the examples I've come across.
Screen shots shown here.
1 - Initial load of 1st tableView from "Home" page properly displays content.
2 - Segued to 2nd tableView where changes were made and saved to CoreData.
3 - Returned to 1st tableView using unwind. Cells are sorted correctly but don't contain updated values.
4 - Left 1st tableView via Back and immediately loaded it again and the screen properly displays content. Just wish it did it without having to "go home & return".
NSFetched Results Controller Delegate:
// MARK: - NSFetchedResultsControllerDelegate
extension CardsTableViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
print("\n **** tableView.beganUpdates called")
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .update:
let cell = tableView.cellForRow(at: indexPath!) as! CardsTableViewCell
configure(cell: cell, for: indexPath!)
print("case .update was called.")
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
print("\n **** tableView.endUpdates called")
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let indexSet = IndexSet(integer: sectionIndex)
switch type {
case .insert:
tableView.insertSections(indexSet, with: .automatic)
case .delete:
tableView.deleteSections(indexSet, with: .automatic)
default: break
}
}
}

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
}

NSFetchedResultsControllerDelegate animating deletion on wrong indexPath

The premise: I have a UITableViewController that conforms to NSFetchedResultsControllerDelegate. I also have a fetched results controller and managed object context as variables in the controller. My tableView displays a table with one section of core data objects from the fetched results controller.
What I'm trying to implement is swipe to delete. The object selected for deletion is actually deleted, however the wrong indexPath is being animated to delete and I don't know why. I currently have the following methods that I believe are relevant:
// This method is being called in viewDidLoad, adding all of the CoreData objects to an array called fetchedResults.
func performFetch() {
do { try fetchedResultsController?.performFetch()
fetchedResults = fetchedResultsController?.fetchedObjects as! [Date]
} catch let error as NSError {
print(error.localizedDescription)
}
}
// tableViewDataSource methods
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let objectToDelete = fetchedResults[indexPath.row]
fetchedResultsController?.managedObjectContext.deleteObject(objectToDelete)
print("commitEditingStyle-indexPath = \(indexPath)")
do { try managedContext.save()
} catch let error as NSError {
print(error.localizedDescription)
}
}
}
// NSFetchedResultsControllerDelegate methods
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject object: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Delete:
if let indexPath = indexPath {
print("didChangeObject indexPath = \(indexPath)")
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
default:
return
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
}
As you can see, I print the indexPath for the tableView:commitEditingStyle method as well as the controller:didChangeObject method. Here are the 2 print statements:
commitEditingStyle-indexPath = {length = 2, path = 0 - 3}
didChangeObject-indexPath = {length = 2, path = 0 - 0}
Why is the didChangeObject method picking up the wrong indexPath? When I swipe to delete the object, the object is deleted at the proper indexPath (in this case 3...) but the table view cell that animates deletion is indexPath 0 (the first cell in my table view). What gives?
Remove all usage of fetchedResults from your code. You are caching the initial set of objects that the FRC knows about, but you aren't tracking additions or removals in that cache. The cache is also a waste of memory because you can always get exactly what you want from the FRC and it also tracks changes.
So, what you're seeing should be what appears to be a random difference and is due to indexing differences between the cache array and the FRC. They should match initially, and if you only ever delete the last item it should be ok, but any other deletion would cause them to fall out of sync.

Resources