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.
Related
I have a typical messaging app where messages are stored as Realm objects. I want to display messages of a conversation in a collection/table view in a safe way observing the results
let results = realm.objects(Message.self).filter(predicate)
// Observe Results Notifications
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
Now, this would work assuming that I display all messages. Since they can be a lot I need to load paginated.
How can I track changes then?
I'm searching for a method to get a sort of id of the changed message, but I couldn't find anything.
Realm objects are lazily loaded so they don't 'take up space' until they are accessed. In our case we have have 1000 objects in results but only display 10 at a time. Those are the 10 that 'take up space'. So it may not be an issue to have a large results dataset.
When you populate a results object from Realm, each object has an index. Think of a results as an array. The first object is index 0, the second object in index 1 etc.
When an object is modified in realm, that information is passed to your observer to which you can then update your UI.
Say we have a PersonClass Realm object that has a persons name and email
PersonClass: Object {
#objc dynamic var name = ""
#objc dynamic var email = ""
}
and we want to display a list of people, and if an email address changes we want to update the UI with that change.
When the app starts, we load all of the people into a Results class var.
override func viewDidLoad() {
super.viewDidLoad()
self.personResults = realm.objects(PersonClass.self)
Then we add an observer to those results so the app is notified of changes. .initial will run when the results have been loaded so it's a good place to populate your dataSource and refresh your tableView.
func doObserve() {
self.notificationToken = self.personResults!.observe { (changes: RealmCollectionChange) in
switch changes {
case .initial: // initial object load complete
if let results = self.personResults {
for p in results {
print(p.name, p.email) //populate datasource, refresh
}
}
break
case .update(_, let deletions, let insertions, let modifications):
for index in deletions {
print(" deleted object at index: \(index)")
}
for index in insertions {
print(" inserted object at index: \(index)")
}
for index in modifications {
print(" modified object at index: \(index)")
let person = self.personResults![index]
print("modified item: \(person.name) \(person.email)")
}
break
case .error(let error):
fatalError("\(error)")
break
}
}
}
In this example, when an person stored at index #2 changes his email, the observer responds to that and prints the name and the new email to console.
But...
Realm is live updating and if you refresh your tableView or even just that row so the cell re-loads from the object, the UI will be updated. I don't know what 'How can I track changes then?' means in your use case, but you could actually remove all of the for loops and just have a tableView.reloadData in the .update section and the UI will be updated as data changes. Or, you could use the index and just update that row.
Keep in mind Realm objects in Results are live and always stay fresh as data changes.
Another note is that many Realm objects will have a unique identifier for the object, defined like this
class PersonClass: Object {
#objc dynamic var person_id = UUID().uuidString
override static func primaryKey() -> String? {
return "person_id"
}
}
which can be used to access that specific object within Realm, but not directly related to your question.
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.
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 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.
I am using a tableView to display a list of people. I am trying to add an alert to confirm that the user actually wants to delete the person and to prevent mistakes. However, when I try to delete the person that is stored with CoreData, there seems to be a problem reloading the view. I get this exception:
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 (2) must be equal to the number of rows contained in that section before the update (2), 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).'
Editing and Delete Function:
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
var deleteRow = indexPath.row
indexPathforDelete = indexPath
let entityDescription = NSEntityDescription.entityForName("People", inManagedObjectContext: managedObjectContext!)
let request = NSFetchRequest()
request.entity = entityDescription
var error: NSError?
var objects = managedObjectContext?.executeFetchRequest(request, error: &error)
if let results = objects {
let personToDelete = results[deleteRow] as! NSManagedObject
let firstName = personToDelete.valueForKey("firstName") as! String
let lastName = personToDelete.valueForKey("lastName") as! String
var message = "Are you sure you would like to delete \(firstName) \(lastName)?\nThis will permanentaly remove all records of "
if(personToDelete.valueForKey("gender") as! String == "Male"){
message = "\(message)him."
}
else{
println(personToDelete.valueForKey("gender") as! String)
message = "\(message)her."
}
var deleteAlert : UIAlertView = UIAlertView(title: "Delete \(firstName) \(lastName)", message: message, delegate: self, cancelButtonTitle: "Cancel")
deleteAlert.addButtonWithTitle("Delete")
deleteAlert.show()
}
save()
} 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
}
}
AlertView Response Function:
func alertView(alertView: UIAlertView, clickedButtonAtIndex buttonIndex: Int){
if(buttonIndex == 1){
managedObjectContext?.deleteObject(personToDelete)
tableView.deleteRowsAtIndexPaths([indexPathforDelete], withRowAnimation: .Fade)
save()
}
setEditing(false, animated: true)
self.navigationItem.leftBarButtonItem = nil
}
tableView number of rows function:
var personToDelete = NSManagedObject()
var indexPathforDelete = NSIndexPath()
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
let entityDescription = NSEntityDescription.entityForName("People", inManagedObjectContext: managedObjectContext!)
let request = NSFetchRequest()
request.entity = entityDescription
var error: NSError?
var objects = managedObjectContext?.executeFetchRequest(request, error: &error)
let results = objects
println("Results Count: \(results!.count)")
return results!.count
}
I think the problem is that you have two variables with the name propertyToDelete: a property that you declare and initialise with a blank NSManagedObject:
var personToDelete = NSManagedObject()
and a local variable that you declare within your commitEditingStyle function:
let personToDelete = results[deleteRow] as! NSManagedObject
It is this local variable to which you assign the object from your results array. But this local variable is destroyed when the function completes, and the AlertView action is deleting the object to which the property points. (The reason I hesitate is that I would expect your context to throw an error when it tries to delete an object that has never been registered with it). Note that by contrast you have only the one variable named indexPathforDelete. This holds the correct value when the AlertView action runs, and consequently the tableView deletes the correct row. That's why you get the error: it has deleted a row, but then finds (because no object has been deleted) it still has the same number of rows as before.
The immediate solution is to use the property within your function, rather than a local variable: just delete let:
personToDelete = results[deleteRow] as! NSManagedObject
But I would also recommend rethinking your approach: you are repeating the same fetch. If all the datasource methods do the same, it will be repeated numerous times when the table view is first built, whenever a cell is scrolled into view, whenever a cell is tapped, etc. This will be costly in terms of performance. You should instead undertake the fetch once (perhaps in viewDidLoad), store the results in an array property, and use that for the table view datasource methods. Alternatively, and perhaps preferably, use an NSFetchedResultsController: it is very efficient and there is boilerplate code for updating the table view when objects are added or deleted.
The documentations of tableView:commitEditingStyle:forRowAtIndexPath: says: "You should not call setEditing:animated: within an implementation of this method. If for some reason you must, invoke it after a delay by using the performSelector:withObject:afterDelay: method."