I am completely at this and out of ideas for about a week now.
I have a CoreData stack with multiple coordinators and managed contexts, and I am attempting to implement a logout feature in the app.
The tableviews that interact with CoreData is inside a TabBarController, and there are on-boarding (i.e. registration & login) NavigationControllers and ViewControllers that lead up to it.
What I am attempting to implement is a logout (i.e. unwind to initial ViewController. I have tried so far - to no avail:
reset() each managedObjectContext individually
every possible combination of tableView.beginUpdates(), tableView.endUpdates(), tableView.reloadData(), frc.performFetch()
Delete each entity in each managedObject then reset() each managedObjectContext individually
set tableView, fetchedResultsController delegates and datasource to nil, then to self on viewDidLoad()
Having a NSNotification that fires just before logging out - deleting everything in CoreData and saving it, effectively updating and emptying the table.
a whole lot more
Is there a way to completely reset CoreData so when the user unwinds to initial ViewController and re-run the whole process I won't get
Serious application error. Exception was caught during Core Data
change processing. This is usually a bug within an observer of
NSManagedObjectContextObjectsDidChangeNotification. attempt to insert
row 9 into section 0, but there are only 9 rows in section 0 after the
update with userInfo (null)
or in the case I emptied the table via Notifications:
Serious application error. Exception was caught during Core Data
change processing. This is usually a bug within an observer of
NSManagedObjectContextObjectsDidChangeNotification. attempt to insert
row 9 into section 0, but there are only 0 rows in section 0 after the
update with userInfo (null)
CoreData works just fine if I logout, close the app, and restart. No errors this way. This makes me think there is a way to completely reset CoreData (and any tableViews, fetchedResultsControllers associated with it) or reset to a pristine state when unwinding to the initial ViewController.
or should I just duplicate the storyboard and have a separate set of on-boarding viewControllers just for logging out so CoreData doesn't reinitialize?
Any ideas? Thanks.
In iOS9 and above you can use destroyPersistentStore and optionally add a new one
func destroyAllData(storeType : String = NSSQLiteStoreType) throws {
guard let storeURL = persistentStoreCoordinator.persistentStores.last?.url else {
print("Missing store URL")
return
}
try persistentStoreCoordinator.destroyPersistentStore(at: storeURL, ofType: storeType)
// try persistentStoreCoordinator.addPersistentStore(ofType: storeType, configurationName: nil, at: storeURL)
}
try this!
static let moduleName = "moduleName"
//Model
lazy var managedObjectModel:NSManagedObjectModel = {
let modelURL = Bundle.main.url(forResource: moduleName, withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
func deleteAllEntities() {
let entities = managedObjectModel.entities
for entity in entities {
delete(entityName: entity.name!)
}
}
func delete(entityName: String) {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try persistentStoreCoordinator.execute(deleteRequest, with: DataCoordinator.shared.managedObjectContext)
debugPrint("Deleted Entitie - ", entityName)
} catch let error as NSError {
debugPrint("Delete ERROR \(entityName)")
debugPrint(error)
}
}
Related
I am attempting to configure CoreData+CloudKit using NSPersistentCloudKitContainer to automatically sync data to/from CloudKit.
Following the Apple guide, it was trivial enough to set up an Entity, add the appropriate capabilities in Xcode, set up the persistentContainer and saveContext in my AppDelegate.
I'm making fetch() and save() calls through the NSManagedObjectContext, and I'm able to save and fetch records without issue. I can see them in the CloudKit dashboard.
However, if I uninstall the app from the simulator and rebuild/reinstall, my fetchRequest (which has no NSPredicate or sorting, just fetching all records) is always returning an empty list. I'm using the same iCloud account, and I've tried both the public and private database scope. If I create a new record and then retry my fetch request I can retrieve that newly created record, but never any of the old records. I'm 100% certain these records are still in the CloudKit database, as I can see them on the CloudKit Dashboard web app.
I took a look at Apple's CoreDataCloudKitDemo app, and it is able to fetch "Post" entities from the CloudKit database after an uninstall/reinstall, so I know it is possible. However, it is using an NSFetchedResultsController, which won't work for my application (mine is a SpriteKit game).
I attempted copying my CoreData+Cloudkit code into a brand new Xcode project and I can reproduce this issue there. Here's my code for reference:
import UIKit
import CoreData
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var persistentContainer: NSPersistentContainer = {
// Create a container that can load CloudKit-backed stores
let container = NSPersistentCloudKitContainer(name: "coredatacloudkitexample")
// Enable history tracking and remote notifications
guard let description = container.persistentStoreDescriptions.first else {
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.cloudKitContainerOptions?.databaseScope = .public
container.loadPersistentStores(completionHandler: { (_, error) in
guard let error = error as NSError? else { return }
fatalError("###\(#function): Failed to load persistent stores:\(error)")
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = "nibbler"
// Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
return container
}()
}
// ------
import UIKit
import CoreData
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
let people: [Person]
do {
people = try viewContext.fetch(fetchRequest)
print("---> fetched People from CoreData: \(people)")
// people.isEmpty is ALWAYS true (empty array) on first install of app, even if records exist in table in CloudKit
if people.isEmpty {
let person = Person(context: viewContext)
person.name = "nibbler"
// save the data through managed object context
do {
try viewContext.save()
print("--> created Person in CoreData: \(person)")
} catch {
print("---> failed to save Person: \(error.localizedDescription)")
}
}
} catch {
print("---> error: \(error)")
}
}
}
What am I missing? Why can I only fetch the records created during this app's install and not prior ones?
UPDATE: It seems that if I wait for a few seconds and re-try my fetch on the first app install that I am able to retrieve the results from the CloudKit database. I can also see a vast number of CoreData+CloudKit log messages in the console upon first launch. Here's what I'm thinking -- even when using NSPersistentCloudKitContainer, a fetch() is reading/writing to the local CoreData store, and then a separate process is running in the background to mirror and merge the local CoreData records with the CloudKit records.
As such, I believe I need to somehow wait/be notified that this sync/merge of local CoreData and CloudKit records has completed on first launch before making my fetch() call, rather than making the fetch() call immediately as the app opens. Any ideas?
You need to use NSFetchedResultsController, why do you think it wouldn't work for your application?
The reason it is necessary is NSFetchedResultsController monitors the viewContext and when the sync process downloads new objects and inserts them into a background context the automaticallyMergesChangesFromParent merges the objects in to the viewContext and advances the generation token. The FRC's delegate methods are called to notify you if objects are inserted, updated or deleted from the fetched objects array which are objects in the context that match the fetch request's entity and predicate.
Here is the thing #professormeowingtons you mention whenever you delete the app on simulator you it won't show the previous records, so my suggestion is to try your app on a real device with an iCloud account already configured, that way you'll be able to add some records to your db then delete the app, reinstall and fetch all the previous records you did enter.
What you can try is this:
Set NSPersistentCloudKitContainerOptions
let id = "iCloud.yourid"
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: id)
description?.cloudKitContainerOptions = options
Initialize your CloudKit schema (this is required at least once)
do {
try container.initializeCloudKitSchema()
} catch {
print("ERROR \(error)")
}
Edit:
Can you change lazy var persistentContainer: NSPersistentContainer
to lazy var persistentContainer: NSPersistentCloudKitContainer
Note:
This post does not apply since I actually use CoreData.
In this post the last answer suggests to fetch all items in the new background thread before adding new objects, but this is done in my code.
This post suggests to unfault an item before its context is saved, but this is also done in my code.
My app uses CoreData to store objects called shoppingItems. I wrote a class CoreDataManager that initialises CoreData and has essentially one function to overwrite the currently stored items, and one function to fetch all items. Both functions operate in the background, i.e. on a separate thread.
Here is my code (irrelevant parts are left out).
I setup core data on the main thread:
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
})
return container
}()
This is the write function:
func overwriteShoppingItems(_ shoppingItems: Set<ShoppingItem>, completion: #escaping (Error?) -> Void) {
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let viewContext = self.persistentContainer.viewContext
backgroundContext.parent = viewContext
backgroundContext.performAndWait {
// Delete currently stored shopping items
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: CDEntityShoppingItem)
do {
let result = try backgroundContext.fetch(fetchRequest)
let resultData = result as! [NSManagedObject]
for object in resultData {
backgroundContext.delete(object)
}
if !shoppingItems.isEmpty {
// Save shopping items in managed context
let cdShoppingItemEntity = NSEntityDescription.entity(forEntityName: CDEntityShoppingItem, in: backgroundContext)!
for nextShoppingItem in shoppingItems {
let nextCdShoppingItem = CDShoppingItem(entity: cdShoppingItemEntity, insertInto: backgroundContext)
nextCdShoppingItem.name = nextShoppingItem.name
}
}
let saveError = self.saveManagedContext(managedContext: backgroundContext)
completion(saveError)
} catch let error as NSError {
completion(error)
}
}
}
func saveManagedContext(managedContext: NSManagedObjectContext) -> Error? {
if !managedContext.hasChanges { return nil }
do {
try managedContext.save()
return nil
} catch let error as NSError {
return error
}
}
And this is the fetch function:
func fetchShoppingItems(completion: #escaping (Set<ShoppingItem>?, Error?) -> Void) {
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let viewContext = self.persistentContainer.viewContext
backgroundContext.parent = viewContext
backgroundContext.performAndWait {
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
do {
let cdShoppingItems: [CDShoppingItem] = try backgroundContext.fetch(fetchRequest)
guard !cdShoppingItems.isEmpty else {
completion([], nil)
return
}
for nextCdShoppingItem in cdShoppingItems {
}
completion(shoppingItems, nil)
} catch let error as NSError {
completion(nil, error)
}
}
}
In normal operation, the code seems to work.
The problem:
I also wrote a unit test that tries to provoke multi-threading problems. This test uses a concurrent dispatch queue:
let concurrentReadWriteQueue = DispatchQueue(label: „xxx.test_coreDataMultithreading", attributes: .concurrent)
A timer defines the test time.
In the scheme for tests, I have set the arguments -com.apple.CoreData.Logging.stderr 1 and -com.apple.CoreData.ConcurrencyDebug 1.
During the test time, overwriteShoppingItems and fetchShoppingItems are inserted repeatedly in the queue, and executed concurrently.
This unit test executes a few reads and writes, before it stops at the line
let itemName = nextCdShoppingItem.name!
because nextCdShoppingItem.name is nil, which should never happen, because I never store nil.
Immediately before the crash, the following is logged:
CoreData: error: API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x600000e6c980, store PSC = 0x0)
If I do only fetches, or only writes, the CoreData warning is not logged. Thus it seems definitely to be a multi-threading issue.
However, CoreData.ConcurrencyDebug does not detect it.
It looks as if during a fetch operation on one thread, the other thread deletes the currently fetched item, so that its properties are read back as nil.
But this should not happen because fetches and saves are done with backgroundContext.performAndWait, i.e. serially.
And the stack trace shows that only a single thread accesses CoreData: Thread 3 Queue : NSManagedObjectContext 0x600003c8c000 (serial)
My questions:
What is my misuse of the CoreData API, and how to do it right?
Why is an item sometimes read back as nil?
EDIT:
Maybe this helps to identify the problem: When I comment out backgroundContext.delete(object) in overwriteShoppingItems, the error is no longer logged, and no item is fetched as nil.
Okay so i got same error but only while "archiving" the project. Took me several hours to find the issue, since I was getting the error only on uploading, in the assets catalog, I had some duplicate images, by removing them the error is gone.
Anyone else getting this error AND your Coredata setup is fine, check your assets folder.
If you are not using core data in your project then check your assets files are proper or not.
I am also getting error like this then i have findout this error from my project.
Problem seems to be solved.
It happened apparently, because the functions overwriteShoppingItems and fetchShoppingItems both setup a separate background context with let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) with their own queue so that fetches and saves were not serialized by a single queue.
I modified my code now in the following way:
I have now a property
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
that is initialized as
self.backgroundContext.persistentStoreCoordinator = self.persistentContainer.persistentStoreCoordinator
self.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
and fetches and saves are done using
backgroundContext.perform {…}
Now the CoreData error is no longer logged, and no item is fetched as nil.
In my case I created an new managedObjectContext without (yet) assigning a persistent store. Eventually I called -[psc metadataForPersistentStore:store] with the store being nil. This caused the log message to be written.
(side-note: you can always catch these outputs by setting a breakpoint on fprintf)
If you are seeing this during tear-down of a Core Data stack, check for un-removed observers or memory leaks.
In my case, I had managed objects which were each observing themselves – one of their relationships, using Swift's Key-Value Observing (NSKeyValueObservation objects). (Example use case: When the last a department's employees are deleted, delete the department.) The console messages appeared while the containing Core Data document was being closed. By breaking on fprintf I could see that the message was logged when a notification was being sent to an observer, and the number of messages was always equal to the number of non-faulted such objects in the managed object context (5 departments --> 5 console messages, etc., etc.). The problem was that I was not removing these observations, and the solution was of course to implement func willTurnIntoFault() in the observing objects, and in this func remove the observation.
In another situation, these messages stopped after I fixed a memory leak, but I did not study that in detail.
The full error messages were: error: API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x600002c5cc00, store PSC = 0x0)
CoreData: error: API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x600002c5cc00, store PSC = 0x0) and they occurred during the call to NSPersistentStoreCoordinator.remove(_:). So, this error is telling me that the store's PSC was nil, but I checked this with the debugger and found that the store's PSC was not nil before the call. I filed a bug to Apple on this (FB11669843) asking if they could improve the error message.
An update is requested, since this question has of course been answered for previous versions, the latest search result dated 12/16 generates irrelevant compatibility with previous iOS 9 and 10 projects.
The documentation of course says to select the Use Core Data checkbox when starting a new project, which I did not select, but now think iCloud + Core Data needs to be added to take my app to its next phase -> wherein something like NSFileCoordinator and NSFilePresenter is needed, since in my app UI users are presented with a number of TOPICS, each having three OPTIONS, regarding which users are to choose one option. For each topic the UI then displays the TOTAL NUMBER of users who have chosen each option and the PERCENTAGE of the total for each option.
Right now, the number of choices for each option and the percentage of the total are of course just calculated in my native app -> but actually need to be CALCULATED in something central like the cloud or most likely on a website…but then the website raises the simultaneous read/write problems that NSFileCoordinator and NSFilePresenter have already solved.
So if the iCloud + Core Data system can interject basic arithmetic calculations on the existing Ubiquitous Container numerical value totals - in the cloud upon receiving write numerical value commands from individual users - before sending out the new Ubiquitous Container numerical total and percent values - then I’d much appreciate advise on how fix the errors generated below in trying Create and Initialize the Core Data Stack. Otherwise guess I’ll have to scrape Xcode and go to a hybrid app like PhoneGap if that's the best one for the job.
Hence, referring to the Core Data Programming Guide:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/InitializingtheCoreDataStack.html#//apple_ref/doc/uid/TP40001075-CH4-SW1
and pasting in the following code in the beginning of my existing project, generates
Use of unresolved identifier ‘persistentContainer’… ‘managedObjectContext’
... errors. And the line
init(completionClosure: #escaping () -> ()) {
... generates
Initializers may only be declared within a type
import UIKit
import CoreData
class DataController: NSObject {
var managedObjectContext: NSManagedObjectContext
init(completionClosure: #escaping () -> ()) {
persistentContainer = NSPersistentContainer(name: "DataModel")
persistentContainer.loadPersistentStores() { (description, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
completionClosure()
}
}
}
init(completionClosure: #escaping () -> ()) {
//This resource is the same name as your xcdatamodeld contained in your project
guard let modelURL = Bundle.main.url(forResource: "DataModel", withExtension:"momd") else {
fatalError("Error loading model from bundle")
}
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
managedObjectContext = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = psc
let queue = DispatchQueue.global(qos: DispatchQoS.QoSClass.background)
queue.async {
guard let docURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last else {
fatalError("Unable to resolve document directory")
}
let storeURL = docURL.appendingPathComponent("DataModel.sqlite")
do {
try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: nil)
//The callback block is expected to complete the User Interface and therefore should be presented back on the main queue so that the user interface does not need to be concerned with which queue this call is coming from.
DispatchQueue.main.sync(execute: completionClosure)
} catch {
fatalError("Error migrating store: \(error)")
}
}
}
// followed by my existing working code:
class ViewController: UIViewController {
go to File > new file... select core Data under iOS and select Data Model
you'll still need some code which xcode auto generates whenever you select core data during project creation.
to get it, just create new project with core data option checked and copy all the code written under ** //Mark: - Core Data Stack** comment in AppDelegate.swift
and add
import CoreData
above
OPTIONAL
And don't forget to change the name of the app after copying the completion block for lazy var persistentContainer. Change the name of your app on this part *NSPersistentContainer(name: "SHOULD-BE-THE-NAME-OF-YOUR-APP") And managedObjectModel function of the code you just copied**
If you're lazy like me, here's all the code you need to copy from the new Core Data project... (why make everyone create a new project?). Change YOUR_APP_NAME_HERE
At the top of your AppDelegate.swift file:
import CoreData
At the bottom of AppDelegate.swift file, before the ending curly bracket:
// MARK: - Core Data stack
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
self.saveContext()
}
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "YOUR_APP_NAME_HERE")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
I know this is answered, but I believe the actual problem is with Apple's Documentation. If you compare the Objective-C code to the Swift code, you will see that var managedObjectContext: NSManagedObjectContext is not actually defined. You should replace that line with var persistentContainer: NSPersistentContainer. This is the Objective-c interface
#interface MyDataController : NSObject
#property (strong, nonatomic, readonly) NSPersistentContainer *persistentContainer;
- (id)initWithCompletionBlock:(CallbackBlock)callback;
#end
So DataController.swift should be:
class DataController: NSObject {
// Delete this line var managedObjectContext: NSManagedObjectContext
var persistentContainer: NSPersistentContainer
init(completionClosure: #escaping () -> ()) {
persistentContainer = NSPersistentContainer(name: "DataModel")
persistentContainer.loadPersistentStores() { (description, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
completionClosure()
}
}
}
As for the rest of your code, it's not necessary Apple Docs.
Prior to iOS 10 and macOS 10.12, the creation of the Core Data stack was more involved
That section of code is showing you the old way.
Use the following code
lazy var persistantCoordinator :NSPersistentStoreCoordinator = {
let poc = NSPersistentStoreCoordinator(managedObjectModel:managedObjectModel)
let documentFolderUrl = FileManager.default.urls(for: .documentDirectory, in:.userDomainMask).last
let path = documentFolderUrl!.appendingPathComponent("Database.sqlite")
let options = [NSMigratePersistentStoresAutomaticallyOption: true,NSInferMappingModelAutomaticallyOption: true]
do{
try poc.addPersistentStore(ofType:NSSQLiteStoreType, configurationName: nil, at: path, options: options)
}catch{
print(error.localizedDescription)
}
return poc
}()
private lazy var managedObjectModel:NSManagedObjectModel = {
let url = Bundle.main.url(forResource:"Database", withExtension:"momd")
return NSManagedObjectModel(contentsOf:url!)!
}()
fileprivate lazy var managedObjectContext:NSManagedObjectContext = {
let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
moc.persistentStoreCoordinator = persistantCoordinator
return moc
}()
I am currently learning Swift 3 for an Assignment.
I have created a master-view prototype in Xcode. It loads one entity from Core-Data (a custom Event) using custom cells (just two labels for a name and timestamp).
currently it deletes all existing entities in the context, then generates 5 new ones:
override func viewDidLoad() {
super.viewDidLoad()
// ...
// removes all items from the database core
let context = self.fetchedResultsController.managedObjectContext
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Event")
let request = NSBatchDeleteRequest(fetchRequest: fetch)
do
{
// not sure what to do with result
let _ = try context.execute(request)
try context.save()
// get count of event entities from core context...
print("entities in context: \(try context.count(for: NSFetchRequest<NSFetchRequestResult>(entityName: "Event")))")
}
catch
{
let nserror = error as NSError
print("encountered error clearing all existing events from app Core Data.")
}
// load a fresh batch of items
insertFiveNewEvents()
}
func insertFiveNewEvents() {
let context = self.fetchedResultsController.managedObjectContext
for c in 1...5
{
let newEvent = Event(context: context)
// If appropriate, configure the new managed object.
newEvent.name = "Item" + String(c)
newEvent.timestamp = NSDate()
// Save the context.
do {
try context.save()
print("entities in context: \(try context.count(for: NSFetchRequest<NSFetchRequestResult>(entityName: "Event")))")
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
console output from the above and insertFiveNewEvents() is as follows:
entities in context: 0 -- after clearing core context
entities in context: 1
entities in context: 2
entities in context: 3
entities in context: 4
entities in context: 5 -- after inserting new entities
An error is occurring when the table is loading cells.
fatal error: unexpectedly found nil while unwrapping an Optional value
Screen snip of console and crash point:
So it's loading and configuring the cells, it configured the first 5 fine (the five new events in core), but then it tries to load another.
A debugger watch sows that event.name! is invalid, suggesting event does not reference an existing entity.
I am unsure of where it is getting these entities from.
Possibilities:
I'm not working with the core context properly.
I have to do something with UITableViewController after removing and adding events. I did try tableView.reload() before but it had no effect.
the tableview thinks there are more cells than events.
Debugging also seems to show then when the fetchedResultsController is asked for a section, count and row count per section, it is returning multiple sections with 8 rows each. I'm not sure if this is an issue (because I don't fully understand the sections and rows for the tableview).
Suggestions?
I have an NSFetchedResultsController and I am trying to update my data on a background context. For example, here I am trying to delete an object:
persistentContainer.performBackgroundTask { context in
let object = context.object(with: restaurant.objectID)
context.delete(object)
try? context.save()
}
There are 2 things I don't understand:
I would have expected this to modify, but not save the parent context. However, the parent context is definitely being saved (as verified by manually opening the SQLite file).
I would have expected the NSFetchedResultsController to update when the background content saves back up to its parent, but this is not happening. Do I need to manually trigger something on the main thread?
Obviously there is something I am not getting. Can anybody explain this?
I know that I have implemented the fetched results controller delegate methods correctly, because if I change my code to directly update the viewContext, everything works as expected.
Explanation
NSPersistentContainer's instance methods performBackgroundTask(_:) and newBackgroundContext() are poorly documented.
No matter which method you call, in either case the (returned) temporary NSManagedObjectContext is set up with privateQueueConcurrencyType and is associated with the NSPersistentStoreCoordinator directly and therefore has no parent.
See documentation:
Invoking this method causes the persistent container to create and
return a new NSManagedObjectContext with the concurrencyType set to
privateQueueConcurrencyType. This new context will be associated with
the NSPersistentStoreCoordinator directly and is set to consume
NSManagedObjectContextDidSave broadcasts automatically.
... or confirm it yourself:
persistentContainer.performBackgroundTask { (context) in
print(context.parent) // nil
print(context.persistentStoreCoordinator) // Optional(<NSPersistentStoreCoordinator: 0x...>)
}
let context = persistentContainer.newBackgroundContext()
print(context.parent) // nil
print(context.persistentStoreCoordinator) // Optional(<NSPersistentStoreCoordinator: 0x...>)
Due to the lack of a parent, changes won't get committed to a parent context like e.g. the viewContext and with the viewContext untouched, a connected NSFetchedResultsController won’t recognize any changes and therefore doesn’t update or call its delegate's methods. Instead changes will be pushed directly to the persistent store coordinator and after that saved to the persistent store.
I hope, that I was able to help you and if you need further assistance, I can add, how to get the desired behavior, as described by you, to my answer. (Solution added below)
Solution
You achieve the behavior, as described by you, by using two NSManagedObjectContexts with a parent-child relationship:
// Create new context for asynchronous execution with privateQueueConcurrencyType
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
// Add your viewContext as parent, therefore changes are pushed to the viewContext, instead of the persistent store coordinator
let viewContext = persistentContainer.viewContext
backgroundContext.parent = viewContext
backgroundContext.perform {
// Do your work...
let object = backgroundContext.object(with: restaurant.objectID)
backgroundContext.delete(object)
// Propagate changes to the viewContext -> fetched results controller will be notified as a consequence
try? backgroundContext.save()
viewContext.performAndWait {
// Save viewContext on the main queue in order to store changes persistently
try? viewContext.save()
}
}
However, you can also stick with performBackgroundTask(_:) or use newBackgroundContext(). But as said before, in this case changes are saved to the persistent store directly and the viewContext isn't updated by default. In order to propagate changes down to the viewContext, which causes NSFetchedResultsController to be notified, you have to set viewContext.automaticallyMergesChangesFromParent to true:
// Set automaticallyMergesChangesFromParent to true
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
persistentContainer.performBackgroundTask { context in
// Do your work...
let object = context.object(with: restaurant.objectID)
context.delete(object)
// Save changes to persistent store, update viewContext and notify fetched results controller
try? context.save()
}
Please note that extensive changes such as adding 10.000 objects at once will likely drive your NSFetchedResultsController mad and therefore block the main queue.
The view context will not update unless you have set it to automatically merge changes from the parent. The viewContext is already set as child of any backgroundContext that you receive from the NSPersistentContainer.
Try adding just this one line:
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
Now, the viewContext WILL update after the backgroundContext has been saved and this WILL trigger the NSFetchedResultsController to update.
This works me perfectly in my project.
In function updateEnglishNewsListener(:), here parameter data is in anyobject and i further convert it into json formate for saving purpose.
Core Data uses thread (or serialized queue) confinement to protect managed objects and managed object contexts (see Core Data Programming Guide). A consequence of this is that a context assumes the default owner is the thread or queue that allocated it—this is determined by the thread that calls its init method. You should not, therefore, initialize a context on one thread then pass it to a different thread.
There are three type
1. ConfinementConcurrencyType
2. PrivateQueueConcurrencyType
3. MainQueueConcurrencyType
The MainQueueConcurrencyType creates a context associated with the main queue which is perfect for use with NSFetchedResultsController.
In updateEnglishNewsListener(:) function, params data is your input. (data->Data you wants to update.)
private func updateEnglishNewsListener(data: [AnyObject] ){
//Here is your data
let privateAsyncMOC_En = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
// The context is associated with the main queue, and as such is tied into the application’s event loop, but it is otherwise similar to a private queue-based context. You use this queue type for contexts linked to controllers and UI objects that are required to be used only on the main thread.
privateAsyncMOC_En.parent = managedObjectContext
privateAsyncMOC_En.perform{
// The perform(_:) method returns immediately and the context executes the block methods on its own thread. Here it use background thread.
let convetedJSonData = self.convertAnyobjectToJSON(anyObject: data as AnyObject)
for (_ ,object) in convetedJSonData{
self.checkIFNewsIdForEnglishAlreadyExists(newsId: object["news_id"].intValue, completion: { (count) in
if count != 0{
self.updateDataBaseOfEnglishNews(json: object, newsId: object["news_id"].intValue)
}
})
}
do {
if privateAsyncMOC_En.hasChanges{
try privateAsyncMOC_En.save()
}
if managedObjectContext.hasChanges{
try managedObjectContext.save()
}
}catch {
print(error)
}
}
}
Checking data is already exist in coredata or not for avoiding the redundance data. Coredata have not primary key concept so we sequently check data already exist in coredata or not. Data is updated if and only if updating data is already exist in coredata. Here checkIFNewsIdForEnglishAlreadyExists(:) function return 0 or value . If it returns 0 then data is not saved in database else saved. I am using completion handle for knowing the new data or old data.
private func checkIFNewsIdForEnglishAlreadyExists(newsId:Int,completion:(_ count:Int)->()){
let fetchReq:NSFetchRequest<TestEntity> = TestEntity.fetchRequest()
fetchReq.predicate = NSPredicate(format: "news_id = %d",newsId)
fetchReq.fetchLimit = 1 // this gives one data at a time for checking coming data to saved data
do {
let count = try managedObjectContext.count(for: fetchReq)
completion(count)
}catch{
let error = error as NSError
print("\(error)")
completion(0)
}
}
Replacing the old data to new one according to requirement.
private func updateDataBaseOfEnglishNews(json: JSON, newsId : Int){
do {
let fetchRequest:NSFetchRequest<TestEntity> = TestEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "news_id = %d",newsId)
let fetchResults = try managedObjectContext.fetch(fetchRequest as! NSFetchRequest<NSFetchRequestResult>) as? [TestEntity]
if let fetchResults = fetchResults {
if fetchResults.count != 0{
let newManagedObject = fetchResults[0]
newManagedObject.setValue(json["category_name"].stringValue, forKey: "category_name")
newManagedObject.setValue(json["description"].stringValue, forKey: "description1")
do {
if ((newManagedObject.managedObjectContext?.hasChanges) != nil){
try newManagedObject.managedObjectContext?.save()
}
} catch {
let saveError = error as NSError
print(saveError)
}
}
}
} catch {
let saveError = error as NSError
print(saveError)
}
}
Convertion anyobject to JSON for saving purpose in coredata
func convertAnyobjectToJSON(anyObject: AnyObject) -> JSON{
let jsonData = try! JSONSerialization.data(withJSONObject: anyObject, options: JSONSerialization.WritingOptions.prettyPrinted)
let jsonString = NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue)! as String
if let dataFromString = jsonString.data(using: String.Encoding.utf8, allowLossyConversion: false) {
let json = JSON(data: dataFromString)
return json
}
return nil
}
Hope it will help you. If any confusion then please ask.