I’m using NSFetchedResultsController and DATAStack. My NSFetchedResultsController doesn’t update my table if I make any changes in another contexts.
I use dataStack.mainContext in my NSFetchedResultsController. If I do any updates from mainContext everything is okay. But if I write such code for example:
dataStack.performInNewBackgroundContext({ (backgroundContext) in
// Get again NSManagedObject in correct context,
// original self.task now in main context
let backTask = backgroundContext.objectWithID(self.task.objectID) as! CTask
backTask.completed = true
let _ = try? backgroundContext.save()
})
NSFetchedResultsController doesn’t update cells. I will see updated data if I reload application or whole view for example.
There is no error in data saving (I’ve checked).
I’ve tried to check NSManagedObjectContextDidSaveNotification is received, but if I do .performFetch() after notification is received I again get old data in table.
Finally I’ve got it, it’s not context problem.
I’ve used such type of code in didChangeObject func in NSFetchedResultsControllerDelegate if NSFetchedResultsChangeType is Move (that event was generated because of sorting I think):
guard let indexPath = indexPath, newIndexPath = newIndexPath else { return }
_tableView?.moveRowAtIndexPath(indexPath, toIndexPath: newIndexPath)
And that’s incorrect (I don’t know why NSFetchedController generates move event without «reload data event» if data was changed), I’ve replaced it with next code and everything now is working.
guard let indexPath = indexPath, newIndexPath = newIndexPath else { return }
_tableView?.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
_tableView?.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
It isn't documented terribly well what the different background context options should be used for and how. Likely worth raising an issue / submitting a PR.
Your issue is that performInNewBackgroundContext doesn't merge changes into the main context. This is because the context hierarchy isn't prescribed, so you can choose what you want to do.
So, you can either monitor the save and merge the changes yourself. Or you could set the parent context. Or you could use newBackgroundContext (which does merge changes automatically, even though that's a bit weird...).
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.
Hy, I'm trying to couple RealmNotifications with updating a tableView but for some reason this keeps generating multiple crashes on the tableView because of inconsistency between the number of sections that exist and what the realm notification has sent. This is the code I have for observing any changes on the Results<T>:
do {
let realm = try Realm()
sections = realm.objects(AssetSections.self).filter("isEnabled = true AND (assets.#count > 0 OR isLoading = true OR banners.#count > 0 OR tag == 'My Tools')").sorted(byKeyPath: "sort_order", ascending: true)
guard let sections = sections else { return }
// Watch on the asset sections
notificationToken = sections.observe { [weak self] (change: RealmCollectionChange) in
switch change {
case .initial: break
case .error(let error):
self?.handle(error: error)
case .update(_, let deletions, let insertions, let modifications):
self?.updatedModel.onNext((insertions: insertions, modifications: modifications, deletions: deletions))
}
}
} catch { }
The above code occurs on a ViewModel and a ViewController is observing those changes like so:
vm.updatedModel
.subscribe(onNext: { [weak self] (insertions, modifications, deletions) in
guard let `self` = self else { return }
self.tableView.beginUpdates()
self.tableView.insertSections(insertions, animationStyle: .none)
self.tableView.deleteSections(deletions, animationStyle: .none)
self.tableView.reloadSections(modifications, animationStyle: .none)
self.tableView.endUpdates()
})
.disposed(by: disposeBag)
I'm working with sections instead of Rows because this is a tableView with multiple sections and just one row per section.
The crash I'm getting is if I do a pull to refresh which does multiple network calls which in turn makes multiple changes to the objects. The funny thing is I can always almost replicate the crash if I scroll down rapidly during a pull to refresh. The error is the following:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete section 14, but there are only 14 sections before the update'
The way I get the numberOfSections for the tableView is the following:
var numberOfSections: Int {
return sections?.count ?? 0
}
My suspicion is that since the notifications are getting delivered on the next runLoop of the Main Thread and since I'm making the thread busy by scrolling and messing with the UI by the time I get a notification and the tableView reacts to it, it's already out of sync. But I'm not exactly sure if this is the problem or if it is how to solve it.
Thank you
Edit
One way to avoid this is just .reloadData() on the tableView but it's a perfomance hit especially on big datasets, and I can't use the default tableView animations. To diminish the perfomance hit of calling .reloadData() multiple times I'm using debounce.
vm.updatedModel
.debounce(1.0, scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] (insertions, modifications, deletions) in
guard let `self` = self else { return }
self.tableView.reloadData()
})
.disposed(by: disposeBag)
Great question, and it looks like the Realm documentation does exactly as you do (with rows rather than sections) but doesn't address this. I can't see an obvious answer, but best I can do is these possible workarounds (although tbh, neither is great).
Rework the update code to pull together results, then do a single commit when they are complete.
Make a static copy of sections to use locally, and update it within the subscription. i.e. copy the RealmCollection to an array, which then is not a dynamic view into the realm. This will stay synchronised to your updates.
Best I can do. Otherwise, I can't see how you can guarantee synchronisation between the dynamic query and the notification.
Full title: Swift 4.2 using split view controller, issue with "repeat" Notification on first load that disrupts ability to programmatically select a "new" object in the Master Table View.
Background
I am working with a split view controller with master and detail table views - master is a plain dynamic table view that displays a list of entities to the user and detail is a grouped dynamic table view that displays the attributes and relationship values for the user-selected entity in the master.
I have implemented Core Data.
The master table view shows a list of entities. The data source for the master table view is a fetched results controller.
The detail table view shows the attributes and associated relationship values of the currently selected row (entity) in the master table view. The data source for the detail table view is the entity relating to the currently selected row in the master table view, which it is passed using a "Show Detail" segue.
On devices with larger screens where splitViewController.isCollapsed == false, the master and detail table views are both active on screen, per the image below.
Fairly standard arrangement for a data driven app...?
Logic Flow
When a user updates an existing entity (in the case of the screenshot example, an existing "Event"), they click the the Save button in the navigation bar of the detail table view.
Because the entity already exists, in the Master Table View Controller's controller(_:didChange:at:for:newIndexPath) Fetched Results Controller Delegate method:
under case .update, implement tableView.reloadRows(at: [indexPath!], with: UITableViewRowAnimation.none), which triggers tableView(_willDisplay:forRowAt:) which in turn updates the formatting of the selected Table View Cell; AND
under case .insert, case .update and case .move, I set a temporary value for the current IndexPath indexPathForManagedObjectChanged
The Fetched Results Controller Delegate method:
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: .fade)
indexPathForManagedObjectChanged = newIndexPath
print("___controllerDidChangeObject: INSERTED OBJECT")
case .delete:
tableView.deleteRows(at: [indexPath!], with: .fade)
print("___controllerDidChangeObject: DELETED OBJECT")
case .update:
configureCell(tableView.cellForRow(at: indexPath!)!, withEvent: anObject as! PT_Events)
indexPathForManagedObjectChanged = indexPath
tableView.reloadRows(at: [indexPath!], with: UITableView.RowAnimation.none)
print("___controllerDidChangeObject: UPDATED OBJECT")
case .move:
configureCell(tableView.cellForRow(at: indexPath!)!, withEvent: anObject as! PT_Events)
indexPathForManagedObjectChanged = newIndexPath
tableView.moveRow(at: indexPath!, to: newIndexPath!)
print("___controllerDidChangeObject: MOVED OBJECT")
}
}
Then, in the Master Table View Controller's controllerDidChangeContent(_:) Fetched Results Controller Delegate method, I execute the following code...
guard let indexPathOfRowToSelect: IndexPath = indexPathForManagedObjectChanged else {
return
}
indexPathForManagedObjectChanged = nil
tableView.selectRow(at: indexPathOfRowToSelect, animated: false, scrollPosition: UITableViewScrollPosition.none)
This ensures that after inserts, updates and moves, the row that corresponds to the data in the Detail Table View is selected.
I use a Notification of type UserDefaults.didChangeNotification.
The observer is added in the Master Table View Controller's (EDIT was viewDidLoad, but now) viewWillAppear(_:) method. The observer is removed in the Master Table View Controller's viewWillDisappear(_:) method.
If the user changes one of the settings the Notification observer calls the following function...
#objc
func userDefaultSettingsDidChange(_ notification: Notification) {
if (notification.object as? UserDefaults) != nil {
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: classCacheName)
fetchedResultsController = nil
tableView.reloadData()
}
}
Problem
On first run of the Master Table View Controller there is a Notification that calls the userDefaultSettingsDidChange method, but it calls this AFTER the first run through of the Fetched Results Controller Delegate methods in response to the first user interaction. This despite no change to the user defaults.
The issue occurs on simulator and on device.
I've added a stack of print()s (removed for ease of reading code) to track what is happening in what order.
The console logs the userDefaultSettingsDidChange function as executing AFTER the Fetched Results Controller Delegate methods have completed - BUT only on first run.
Because of this, the call to reloadData wipes out my previous call to selectRow.
Solution
I can wrap my call to the selectRow(at:animated:scrollPosition:) instance method in the Master Table View Controller's controllerDidChangeContent(_:) Fetched Results Controller Delegate method to include a slight delay...
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
guard let indexPathOfRowToSelect: IndexPath = indexPathForManagedObjectChanged else {
return
}
indexPathForManagedObjectChanged = nil
let when = DispatchTime.now() + 0.20
DispatchQueue.main.asyncAfter(deadline: when) {
self.tableView.selectRow(at: indexPathOfRowToSelect, animated: false, scrollPosition: UITableView.ScrollPosition.none)
}
}
This works!
But - I'm not satisfied.
I've done a lot of reading but for some reason, cannot seem to understand what is happening in NotificationCenter that is causing this ghost update to user settings despite no update.
Could someone please explain why I'm seeing this delayed "update" to user settings that is wreaking havoc on only the first user interaction in my UI?
The problem I'm seeing is that when I create a UIContextualAction with .destructive and pass true in completionHandler there seems to be a default action for removing the row.
If you create a new Master-Detail App from Xcode's templates and add this code in MasterViewController...
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let testAction = UIContextualAction(style: .destructive, title: "Test") { (_, _, completionHandler) in
print("test")
completionHandler(true)
}
return UISwipeActionsConfiguration(actions: [testAction])
}
the row you swipe will be removed. Notice that there's no code there updating the table view. Also the model is not updated and if you scroll way up to cause the row to be reloaded it will reappear.
Passing false in this case does not remove the row. Or using the .normal style and true also does not remove the row.
.destructive and true results in the row being removed by default.
Can anyone explain this behaviour? Why is the row being removed?
Per the documentation for the Destructive option:
An action that deletes data or performs some type of destructive task.
The completion is meant to signify if the action was a success. Passing true would mean that the destructive task was a success and thus the row should be removed.
You are meant to manually update your dataSource when the destructive action occurs and not doing so would cause scrolling to make the data reappear. You will also need to tell the tableView that the data has been deleted.
Below is some code showing a working example:
UIContextualAction(style: .destructive, title: "Delete") { [weak self] (_, _, completion) in
if self?.canDelete(indexPath) { // We can actually delete
// remove the object from the data source
self?.myData.remove(at: indexPath.row)
// delete the row. Without deleting the row or reloading the
// tableview, the index will be off in future swipes
self?.tableView?.deleteRows(at: [indexPath], with: .none)
// Let the action know it was a success. In this case the
// tableview will animate the cell removal with the swipe
completion(true)
} else { // We can't delete for some reason
// This resets the swipe state and nothing is removed from
// the screen visually.
completion(false)
}
}
Then I need to reload the tableview or call deleteRows in order to have the indexPath be properly computed on the next swipe.
If I have 10 rows and I swipe the 5th one to delete, every one after that will be off by one row unless the tableview is reloaded or the tableview is told of the row being removed in some way.
I can't reproduce this issue in iOS 13. Either the behavior in iOS 12 and before was a bug or it has simply been withdrawn (perhaps because it was confusing).
In iOS 13, if you just return from a .destructive action by calling completion(true) without doing anything, nothing happens and that's the end of the matter. The cell is not deleted for you.
I agree with the answer by Kris Gellci, but notice that if you are using a NSFetchedResultsController it may complicate things. It seems that for a destructive UIContextualAction the call completion(true) will delete the row, but so may the NSFetchedResultsController's delegate. So you can easily end up with errors in that way. With NSFetchedResultsController I decided to call completion(false) (to make the contextual menu close), regardless of whether the action was a success or not, and then let the delegate take care of deleting the table row if the corresponding object has been deleted.
Use the below flag for disabling full swipe.
performsFirstActionWithFullSwipe
By default it will automatically performs the first action which is configured in UISwipeActionsConfiguration. so if you want to disable full swipe delete then set "performsFirstActionWithFullSwipe" as false.
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let test = UIContextualAction(style: .destructive, title: "test") { (action, view, completion) in
// Your Logic here
completion(true)
}
let config = UISwipeActionsConfiguration(actions: [test])
config.performsFirstActionWithFullSwipe = false
return config
}
Hope this will solve your problem.
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.