I currently have a problem that when I try to delete a row from a table, my app crashes and throws an index out of range error. The data deletes fine from the firebase database and the table, but it still throws the error.
The database is designed to store playlists, and each playlist has a dictionary of keys to the name of the song. Here is my firebase structure:
And below is the swift code (line where error is occurring has a comment)
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let song = songs[indexPath.row]
var songKey = ""
if(editingStyle == .delete){
let ref = Database.database().reference().child("Playlists/\(playlist!)")
ref.observe(.value, with: {
snapshot in
let someData = snapshot.value! as! [String: String]
for data in someData {
if(data.value == song){
songKey = data.key
break
}
}
ref.child(songKey).removeValue()
self.songs.remove(at: indexPath.row) //error on this line
self.myTableView.deleteRows(at: [indexPath], with: .fade)
self.myTableView.reloadData()
})
}
}
Thanks for any help!
I think what you should do is take another approach - handle the delete operation within its completion handler. This way you will make sure your data will be consistent. Because what happens if there is an error on your Firebase call? You have to handle this case as well. So do something like this and see what happens:
ref.child(songKey).removeValue { [weak self] error, _ in
if let error = error {
print("There was an error: ", error)
return
}
self?.songs.remove(at: indexPath.row)
self?.tableView.reloadData()
}
What I think is happening here is that your code enters an infinite loop - you use observe on your playlists then you delete. So observe's completion handler is called again and delete is called again. After you deleted the item at this index, the index cannot be found on your array anymore. So just get your playlists from the database without observing any further changes.
In this case try using observeSingleEvent.
Related
something goes wrong when trying to update rows of tableview after delete of Firebase data.
Below is method I use.
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in
let cell = self.messages[indexPath.row]
let b = cell.msgNo
let action = MyGlobalVariables.refMessages.child(MyGlobalVariables.uidUser!)
action.queryOrdered(byChild: "msgNo").queryEqual(toValue: b).observe(.childAdded, with: { snapshot in
if snapshot.exists() { let a = snapshot.value as? [String: AnyObject]
let autoId = a?["autoID"]
action.child(autoId as! String).removeValue()
self.messages.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
} else {
print("snapshot empty")
}}) }
...
return [delete, edit, preview]
}
Initially I checked whole logic without including line /*action.child(autoId as! String).removeValue()*/ then it works normally and removes rows as should be. But once I add this line it removes data from Firebase but tableview is updated in strange way by adding new rows below existing
My guess is that somewhere else in your application you have code like action .observe(.value, which shows the data in the table view. When you delete a node from the database, the code that populates the database gets triggered again, and it adds the same data (minus the node that you removed) to the table view again.
When working with Firebase it's best to follow the command query responsibility segregation principle, meaning that you keep the code that modifies the data completely separate from the flow that displays the data. That means that your code that deletes the data, should not try to update the table view. So something more like:
let action = MyGlobalVariables.refMessages.child(MyGlobalVariables.uidUser!)
action.queryOrdered(byChild: "msgNo").queryEqual(toValue: b).observe(.childAdded, with: { snapshot in
if snapshot.exists() { let a = snapshot.value as? [String: AnyObject]
let autoId = a?["autoID"]
action.child(autoId as! String).removeValue()
} else {
print("snapshot empty")
}}) }
All the above does is remove the selected message from the database.
Now you can focus on your observer, and ensuring it only shows the messages once. There are two options for this:
Always clear self.messages when your .value completion handler gets called before you add the messages from the database. This is by far the simplest method, but may cause some flicker if you're showing a lot of data.
Listen to the more granular messages like .childAdded and .childRemoved and update self.messages based on those. This is more work in your code, but will result in a smoother UI when there are many messages.
Hi I want to remove a row from core data. I was able to remove the item from the table, but not from core data. A lot of places give the same answer to this question. But after struggling for hours and seeing all links from the first 20 pages of google search results, it is still not working.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCellEditingStyle.delete {
foodItems.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
}
let delegate = UIApplication.shared.delegate as! AppDelegate
let managedObjectContext = delegate.persistentContainer.viewContext
let coord = delegate.persistentStoreCoordinator // App delegate has no member persistentStoreCoordinator
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "ProductName")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try coord.executeRequest(deleteRequest, withContext: managedObjectContext)
} catch let error as NSError {
debugPrint(error)
}
}
I hope someone can help me. Thanks in advance.
The docs tell you how to update your in-memory objects: https://developer.apple.com/library/content/featuredarticles/CoreData_Batch_Guide/BatchDeletes/BatchDeletes.html
make sure the resultType of the NSBatchDeleteRequest is set to NSBatchDeleteRequestResultType.resultTypeObjectIDs before the request is executed.
do {
let result = try moc.execute(request) as? NSBatchDeleteResult
let objectIDArray = result?.result as? [NSManagedObjectID]
let changes = [NSDeletedObjectsKey : objectIDArray]
NSManagedObjectContext.mergeChangesFromRemoteContextSave(changes, [moc])
} catch {
fatalError("Failed to perform batch update: \(error)")
}
There are a few things going on here. You want to delete an entry. You need the managed object context, which you are already getting:
let managedObjectContext = delegate.persistentContainer.viewContext
You then appear to be trying to get an NSPersistentStoreCoordinator, which you don't need, and which causes your error message:
let coord = delegate.persistentStoreCoordinator // App delegate has no member persistentStoreCoordinator
You're getting this error message because your app delegate doesn't have a property called persistentStoreCoordinator. It's exactly as obvious as the error message sounds.
I'm not completely sure why you're trying to get a persistent store coordinator-- possibly because you're also trying to use a batch delete request even though you say you just want to delete a single row:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "ProductName")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
If you run that request and it succeeds, you will delete every instance of ProductName, not just the one row. You're not telling the request to filter the objects and find only one (or a few). Instead you've set up a batch request that will hit every single instance instead of the selected row.
You could fix the batch request by adding a predicate, but that would be a very unusual way to delete a single row. Generally, you fetch a bunch of managed objects, and if you want to delete one, you tell the managed object to delete that object. There's no need for a batch request on a single object.
If your foodItems array contains instances of NSManagedObject (or subclasses of NSManagedObject), a better approach would be to use the managed object context, which has a delete method. Get the object at foodItems[indexPath.row] and pass it directly to the delete method.
I posted a post similar to this but my question was half answered. I would like to be able to swipe to delete rows but then save that data to my xcdatamodeld file. I have an attribute in xcdatamodeld called removeTask but that is as far as I got. Any help appreciated
//Removing Tasks
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCellEditingStyle.delete {
// 1)
let task = tasks.remove(at: indexPath.row)
// 2)
saveToCoreData(task: task)
// 3)
tableView.beginUpdates()
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
}
}
func saveToCoreData(task: Task) {
do {
try managedObjectContext.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
You have to delete the object in the data source array and in the Core Data stack:
// 1)
let task = tasks.remove(at: indexPath.row)
managedObjectContext.delete(task)
// 2)
saveToCoreData(task: task)
// 3)
tableView.deleteRows(at: [indexPath], with: .fade)
A side note:
beginUpdates() / endUpdates() is not needed at all for a single delete operation.
// Insert Into CoreData (very important)
let managedObject = NSEntityDescription.insertNewObject(forEntityName: "removedTask", into: self.managedObjectContext)
// assign values
managedObject.value = task.value
These operations are the source of ambiguity.
When saveToCoreData is invoked from commit editingStyle:, it's sole purpose is to pass down the changes made in context to the persistent store.
By performing NSEntityDescription.insertNewObject, a new object is being created in context which is absolutely not the intent here.
Modifying the saveToCoreData as follows will help:
func saveToCoreData(task: Task) {
do {
try managedObjectContext.save()
} catch {
fatalError("Failure to save context: \(error)")
}
// for updating tasks array with the latest data set
let tasksFetch = NSFetchRequest(entityName: "Task") // "Task" or whatever is the actual name of entities stored in tasks array
do {
tasks = try managedObjectContext.executeFetchRequest(tasksFetch) as! [Task]
} catch {
fatalError("Failed to fetch task: \(error)")
}
}
I have two VC with table views, the first showing categories, and the second showing items of selected category (recipes). I am able to get the RecipeTableVC to display filtered data using NSPredicate, but I haven't quite figured out how to delete the recipe from Core Data since the data displayed is a variable containing only the predicated data.
Here is my fetch:
func attemptRecipeFetch() {
let fetchRecipeRequest = NSFetchRequest(entityName: "Recipe")
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRecipeRequest.sortDescriptors = [sortDescriptor]
let controller = NSFetchedResultsController(fetchRequest: fetchRecipeRequest, managedObjectContext: ad.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedRecipeController = controller
do {
try self.fetchedRecipeController.performFetch()
let allRecipes = fetchedRecipeController.fetchedObjects as! [Recipe]
recipesOfCategory = allRecipes.filter { NSPredicate(format: "category = %#", selectedCategory!).evaluateWithObject($0) }
} catch {
let error = error as NSError
print("\(error), \(error.userInfo)")
}
}
So what's populating my table is the recipesOfCategory array.
Here is my attempt to delete so far:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
recipesOfCategory.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
ad.managedObjectContext.delete(recipesOfCategory[indexPath.row])
}
}
This crashes and I understand why, but still haven't come up with a solution. Is there a way to implement swipe to delete where it deletes the recipe from Core Data? Am I using the correct methodology to populate the table with filtered data?
I used the following code to 'Swipe to delete from core data' in a table view for an App I recently did. I may work for you.
In your "tableView:commitEditingStyle",
1. set up CoreData access with ...
let appDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context: NSManagedObjectContext = appDel.managedObjectContext
2. Delete the desired row + incl. from Core Data...
if editingStyle == UITableViewCellEditingStyle.Delete {
context.deleteObject(self.resultsList[indexPath.row]) // Always before
as CoreD
self.resultsList.removeAtIndex(indexPath.row)
do {
try context.save()
} catch {
print("Error unable to save Deletion")
}
} // end IF EditingStyle
self.tableView.reloadData()
In your tableView:commitEditingStyle: you need to just delete the underlying object from Core Data, not from the table view. The NSFetchedResultsController delegate methods will tell you when to remove it from the table view.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell
var query = PFQuery(className:"category")
let object = objects[indexPath.row] as String
query.whereKey("type", equalTo:"DRUM")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
for object in objects {
NSLog("%#", object.objectId)
let abc = object["link"]
println("the web is \(abc)")
cell.textLabel!.text = "\(abc)"
}
} else {
NSLog("Error: %# %#", error, error.userInfo!)
}
}
return cell
}
after add the let object = objects[indexPath.row] as String can't load the view, delete the line show only one row successfully.
First I advise you to get your cell data outside cellForRowAtIndexPath. This function is not a good place to receive data from parse. Make another function and create a class variable and put handle getting data from there.
let object = objects[indexPath.row] as String
for object in objects
Try not to use same variable names for different stuff, as they will confuse you.
This line is not contributing to anything at the moment it seems. Try deleting it:
let object = objects[indexPath.row] as String
First lets have principles in mind. Don't ever update UI from a separate thread, its behavior is unexpected or undefined. It works or works weird.
Second, the problem you have is the when the VC gets loaded the tableView's datasource is called there and then on the main thread. Now you tried to add something on the cell by doing a Async call in separate thread which will take time and main thread is not waiting when the call to parse is being done. If you have difficulty in Async please take a look at the documentation its really important to get a good grasp of the few terms and the principles.
The thing is your main thread runs top to bottom without waiting each call to server thats async in the cell generation. So the result of that call will post later on and you are not posting on main thread too.
Moreover, i would suggest you don't do this approach for big projects or manageable code base. I generally do is:
when the view loads call the Parse with the needed information
Wait for that on a computed variable which i will observe to reload table views once I'm conformed i have the data.
Initially table view will have 0 rows and thats fine. Ill make a spinner dance during that time.
I hope i made some issues clear. Hope it helps you. Cheers!
//a computed var that is initialized to empty array of string or anything you like
//we are observing the value of datas. Observer Pattern.
var datas = [String](){
didSet{
dispatch_async(dispatch_get_main_queue(), {
//we might be called from the parse block which executes in seperate thread
tableView.reloadData()
})
}
}
func viewDidLoad(){
super.viewDidLoad()
//call the parse to fetch the data and store in the above variable
//when this succeeds then the table will be reloaded automatically
getDataFromParse()
}
//get the data: make it specific to your needs
func getDataFromParse(){
var query = PFQuery(className:"category")
//let object = objects[indexPath.row] as String //where do you use this in this block
var tempHolder = [String]()
query.whereKey("type", equalTo:"DRUM")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil && objects != nil {
for object in objects!{
//dont forget to cast it to PFObject
let abc = (object as! PFObject).objectForKey("link") as? String ?? "" //or as! String
println("the web is \(abc)")
tempHolder.append(abc)
}
} else {
print("error") //do some checks here
}
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! UITableViewCell
cell.textLabel!.text = datas[indexPath.row]
return cell
}