When I try to delete a row from the table view using the code below I keep getting a Thread 1: signal SIGABRT error. Im using Swift (Not sure what to make of it) and am building a table view based application. I've been trying to delete a row all day and I'm just not getting anywhere with it. This should be simple task but Swift is making it impossible for me.
// Override to support editing the table view.
override func tableView(tableView: UITableView?, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath?) {
if editingStyle == .Delete {
if let tv = tableView {
if let ip = indexPath {
tv.deleteRowsAtIndexPaths([ip], withRowAnimation: UITableViewRowAnimation.Fade)
}
}
// Delete the row from the data source
} 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
}
}
The error seems to be related to the AppDelegate file:
Are you also updating the data source that backs the table view? It can cause problems if the tableView(numberOfRowsInSection) method doesn't change its return value.
Related
I'm starting to learn CoreData today and I followed a good tutorial on Ray Wenderlich (https://www.raywenderlich.com/145809/getting-started-core-data-tutorial)
The finished project ends with a table view, populated with labels the user enters via an Add button in the nav bar.
I also went ahead and put in a delete function since that wasn't in the tutorial:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath)
{
if editingStyle == .delete
{
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
tableView.beginUpdates()
companies.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
appDelegate.saveContext()
}
}
However when I navigate to a different view controller then return, the deleted object is there again. I have companies initialized as a managed object at the top of the file:
var companies = [NSManagedObject]()
And I thought
companies.remove(at: indexPath.row)
followed by
appDelegate.saveContext()
in the delete function would remove the object from the data source, but like I said it's still there.
How can I delete the object so when I navigate away from the page and come back, its still deleted, but will come back if I restart the app?
EDIT (code that worked for me)
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath)
{
let company = companies[indexPath.row]
if editingStyle == .delete {
managedContext.delete(company)
do {
try managedContext.save()
} catch let error as NSError {
print("Error While Deleting Note: \(error.userInfo)")
}
}
// Fetch new data/reload table
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: companyEntity)
do {
companies = try managedContext.fetch(fetchRequest) as! [NSManagedObject]
} catch let error as NSError {
print("Error While Fetching Data From DB: \(error.userInfo)")
}
tableView.reloadData()
}
The above code works so that objects stay deleted if I navigate off the view then come back, however if I relaunch the app, they're still deleted - I'd like the deleted item to return if the app is relaunched.
seems to me you are not delete object from CoreData at all.
companies.remove(at: indexPath.row) - here you just remove object from the fetched data array
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath)
{
if editingStyle == .delete {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
//remove object from core data
let context:NSManagedObjectContext = appDelegate.managedObjectContext!
context.deleteObject(companies[indexPath.row] as NSManagedObject)
context.save(nil)
//update UI methods
tableView.beginUpdates()
companies.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
appDelegate.saveContext()
}
}
However when I navigate to a different view controller then return, the deleted object is there again
Of course, because you did nothing to change the underlaying data model.
objects stay deleted if I navigate off the view then come back, however if I relaunch the app, they're still deleted - I'd like the deleted item to return if the app is relaunched
I'm sorry but why would you want that? Deleting the item means deleting it! That is, it means that it is deleted from the underlying data model, which is the Core Data database. Why would you bring the dead back to life?
It seems to me that your real problem here is that Core Data is a completely inappropriate underlying data model for what you are really trying to do (whatever it is). Core Data is not a beginner technology. I would suggest that you abandon use of it.
I think your first code was closer to what you want.
The most simple behaviour is to fetch only once, delete from Core Data and delete from the table view to update your UI. That's it, no refetch, no reload.
OR
Fetch, delete from Core Data, refetch and reload the table to see the changes in your UI. Pick one!
If you have an array as intermediate you should delete in it either of course.
Now, you don't need to have an array always, (research about fetched results contoller) you can work directly with Core Data, but luckily it's exactly what you need in this case.
My suggestion: fetch Core Data only once to get the initial data, put everything into the array, use the array to work with, make it the data source of your table and delete items in the array and the table only. So your Core Data won't have anything deleted it will just provide the initial values to your working array.
For the inserts, I suppose you want to keep them. Save them to the array, the table and also Core Data if you wish.
One last thing, you are guarding the AppDelegate in the first lines of code. If your App is running, you have an AppDelegate for sure, no way your app would run without it, it controls your apps life cycle. You may have problems getting a context tough (rare but possible). Guard the context instead :
guard let context = (UIApplication.shared.delegate as? AppDelegate)?.managedObjectContext else {
return
}
Hope it helps.
I have a table view within a VC. It is populated from a core data object using the following code:
// Variables
var allFruits: NSArray
var managedObjectContext: NSManagedObjectContext
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
func loadFruits(){
// Try to load all of the Fruits from the core data.
let fetchRequest = NSFetchRequest()
let entityDescription = NSEntityDescription.entityForName("Fruit", inManagedObjectContext: self.managedObjectContext)
fetchRequest.entity = entityDescription
do{
self.allFruits = try self.managedObjectContext.executeFetchRequest(fetchRequest)
if self.allFruits.count == 0{
print("No saved Fruits")
}
}
catch
{
let fetchError = error as NSError
print(fetchError)
}
}
Then the table view is populated with this specific fruit data. I have got this method for deletion of the Fruits from the table
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if (editingStyle == UITableViewCellEditingStyle.Delete) {
// handle delete (by removing the data from your array and updating the tableview)
managedObjectContext.deleteObject(allFruits[indexPath.row] as! NSManagedObject)
// Attempt to save the object
do{
try appDelegate.managedObjectContext.save()
}
catch let error{
print("Could not save Deletion \(error)")
}
// Remove the deleted item from the table view
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
// Reload the fruits
self.loadFruits()
// Reload the table view
tableView.reloadData()
}
}
This instead just crashes the app every time I try to delete the Fruits.
'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of rows in section 0. The number of rows contained in an
existing section after the update (5) must be equal to the number of
rows contained in that section before the update (5), 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).'
I am suspecting that there is some issue with the fact that I am using an NSArray as opposed to an NSMutableArray.
How can I fix this issue?
Thanks in advance.
First of all use a Swift array rather than a Foundation array
var allFruits = [NSManagedObject]()
This avoids a lot of type casting.
To keep Core Data and the table view in sync you have to delete the object in Core Data and in the model. The solution to completely reload both the model and the view is very expensive and not needed at all.
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let objectToDelete = allFruits[indexPath.row]
allFruits.removeAtIndex(indexPath.row)
managedObjectContext.deleteObject(objectToDelete)
//Attempt to save the object
do{
try appDelegate.managedObjectContext.save()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
catch let error{
print("Could not save Deletion \(error)")
}
}
}
deleteRowsAtIndexPaths includes updating the UI so reloading the table view is not needed.
If the model of the table view is NSManagedObject it's recommended to use NSFetchedResultsController
You should update data source before delete. Like this:
//Reload the fruits
self.loadFruits()
//Remove the deleted item from the table view
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
And if you're sure there's no other changes,it's no need to reload data because this costs a lot.
I fixed my code by simply changing this method:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if (editingStyle == UITableViewCellEditingStyle.Delete) {
// handle delete (by removing the data from your array and updating the tableview)
managedObjectContext.deleteObject(allFruits[indexPath.row] as! NSManagedObject)
//Attempt to save the object
do{
try appDelegate.managedObjectContext.save()
}
catch let error{
print("Could not save Deletion \(error)")
}
//Reload the fruits
self.loadFruits()
//Reload the table view
tableView.reloadData()
}
}
This successfully deletes the object from the core data as well as updating the table view.
Updated for Swift 3 Xcode 8 iOS 10
The following code snippet adds a 'swipe to delete' to your Table Row, and also saves the changes to CoreData.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
if editingStyle == .delete {
context.delete(tasks[indexPath.row] as NSManagedObject)
//objects.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
//Save the object
do{
try (UIApplication.shared.delegate as! AppDelegate).saveContext()
}
catch let error{
print("Cannot Save: Reason: \(error)")
}
//Reload your Table Data
getData()
//Reload the table view
tableView.reloadData()
} 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.
}
}
So I'm trying to write the delete edit behavior for the rows in a tableview. However, when I hit the delete key after selecting a row, the row is not deleted from the tableView. When I try to do it a second time, I get an error saying an unexpected nil value was found.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete { // Handle the Delete action
// Obtain the name of the genre of movie to be deleted
let genre: String = genres[indexPath.section]
// Obtain the list of movies in the genre as AnyObject
let movies: AnyObject? = applicationDelegate.dict_Genres_dict2[genre]
let movArray: [String] = movies?.allKeys as! [String] //The nil value is unwrapped on this line
// Typecast the AnyObject to Swift array of String objects
var moviesOfGenre: Array<String> = movArray
// Delete the identified movie at row
moviesOfGenre.removeAtIndex(indexPath.row)
if moviesOfGenre.count == 0 {
// If no movie remains in the array after deletion, then we need to also delete the genre
applicationDelegate.dict_Genres_dict2.removeObjectForKey(genre)
// Since the dictionary has been changed, obtain the genre names again
genres = applicationDelegate.dict_Genres_dict2.allKeys as! [String]
// Sort the genre names within itself in alphabetical order
genres.sortInPlace { $0 < $1 }
}
else {
// At least one more movie remains in the array; therefore, the genre stays.
// Update the new list of movie for the genre in the NSMutableDictionary
applicationDelegate.dict_Genres_dict2.setValue(moviesOfGenre, forKey: genre)
}
// Reload the rows and sections of the Table View
tableView.reloadData()
}
}
I have marked which line I am receiving the nil value. Any push in the right direction would be most helpful. Thanks!
After removing the array item. You need remove that item from the table too.When you try to delete the array item again, it shows nil, because that item is not available, but the table didn't remove that item from the view.So you need to remove it from table too...
genres.removeAtIndex(indexPath.row) // or section , delete according to your app
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
why do you take section of the item that you want to delete. You didn't delete the genre item from the table. You only deleted the moviesOfGenre. You need to delete the table view item also( genre ). Remove the genre also from the table.
Also you can use swipe to delete function :-
func tableView(tableView: UITableView!, canEditRowAtIndexPath indexPath: NSIndexPath!) -> Bool {
return true
}
func tableView(tableView: UITableView!, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath!) {
if (editingStyle == .Delete) {
// handle delete (by removing the data from your array and updating the tableview)
self.tableView.beginUpdates()
self.genres.removeObjectAtIndex(indexPath.row) // also remove an array object if exists.
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Left)
self.tableView.endUpdates()
}
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()
}
}
I am programming my first iOS application and am in the process of learning about protocols and delegation. The application is a basic random response generator. When the user shakes the device or taps the screen, a random response should display in a label.
Currently, I have three view controllers embedded in a navigation controller: MainViewController.swift, SettingsViewController.swift, and ResponsesViewController.swift.
I have an array in MainViewController.swift that holds the responses. I have successfully been able to pass the data to the third view controller, which is ResponsesViewController.swift, which is a table view for displaying the stored responses. I have set my first view controller, MainViewController as the delegate for my third view controller, ResponsesViewController, and implemented the method where I would like to remove the selected response from the data model (responses array).
My problem is when I try deleting a response I get fatal error: Cannot index empty buffer in the console. I used println(responses.count) to make sure that I was passing the array to the second and third view controllers successfully. I placed the println(responses.count) statement in all three view controllers within the methods viewDidLoad(), viewWillAppear(), and viewDidDisappear(). This showed that the data was passing successfully as there were 3 objects in the array with each println() statement. However, in my delegate, MainViewController.swift, I keep getting the error when I try to remove the selected response from the data model. I placed println(responses.count) into this method, but it keeps returning 0 and crashing with the error. It is only happening when func responsesViewController(controller: ResponsesViewController, didDeleteResponseAtIndexPath indexPath: NSIndexPath) is being called in the delegate (MainViewController.swift)
Here is my code:
MainViewController.swift
var responses: [Response] = []
let response1 = Response(text: "String 1")
let response2 = Response(text: "String 2")
let response3 = Response(text: "String 3")
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBarHidden = true
responses += [response1, response2, response3]
}
* delegate *
func responsesViewController(controller: ResponsesViewController, didDeleteResponseAtIndexPath indexPath: NSIndexPath) {
responses.removeAtIndex(indexPath.row)
}
ResponsesViewController.swift
protocol DeleteResponseDelegate {
func responsesViewController(controller: ResponsesViewController, didDeleteResponseAtIndexPath indexPath: NSIndexPath)
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
delegate?.responsesViewController(self, didDeleteResponseAtIndexPath: indexPath)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
tableView.reloadData()
}
}
-EDIT-
It turns out that I was able to fix my issue by simply passing the array from my third view controller back to my first view controller. I was obviously confused about delegation. Here is my updated code where I deleted the item from the array and then passed that array back to the first view controller:
ResponsesViewController.swift
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
responses.removeAtIndex(indexPath.row)
let mainViewController = navigationController?.viewControllers.first as MainViewController
mainViewController.responses = self.responses
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
tableView.reloadData()
}
}
From your comment -
I had assumed that the responses array in MainViewController still
held the objects that it was created with. Is that incorrect? Does the
data travel across view controllers and only have one instance of the
array at all times?
This would be true of an NSArray, which is an object. Swift arrays are structures, and structures are value types. Value types are copied when they are assigned.
In fact, all of the basic types in Swift—integers, floating-point
numbers, Booleans, strings, arrays and dictionaries—are value types,
and are implemented as structures behind the scenes.
All structures and enumerations are value types in Swift. This means
that any structure and enumeration instances you create—and any value
types they have as properties—are always copied when they are passed
around in your code.”
Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/au/jEUH0.l