Handling Temporary NSManagedObjectID of Core Data` - core-data

I'm trying to retrieve the object ID of a Core Data entity.
I've two Core Data contexts: main(mainQueueConcurrency) and child(privateQueueConcurrency) - I know, it's not the best solution but I'm still learning Core Data so hopefully my next implementation will be better.
Anyways, my aim is to assign the disk location of an image download to the passed entity. Since NSURLSession works on a different thread, my main context disappears. Therefore I go with the child context.
Although since my main context is not saved when I retrieve the object ID, it's temporary and so I can't make the appropriate insertion.
I called some save statements before obtaining the object ID but it didn't made any difference.
The code below outlines my situation. I simplified some places with comments as they were irrelevant to this situation.
How can I make this happen? Thanks.
coreDataStack.managedObjectContext.performBlockAndWait{
for result in jsonCategoriesArray {
if let category = NSEntityDescription.insertNewObjectForEntityForName("Category", inManagedObjectContext: coreDataStack.managedObjectContext) as? Category {
self.insertCategory(category, usingJSON: result, withDeletionFlag: false)
}
}
}
private func insertCategory(category: Category, usingJSON json: JSON, withDeletionFlag deletionFlag: Bool) {
// Assign the text data from JSON into the entity
do {
print("Temporary Object ID: \(category.objectID.temporaryID)")
print("Object ID: \(category.objectID)")
let managedObject = try coreDataStack.managedObjectContext.existingObjectWithID(category.objectID) as? Category
} catch {
print(error)
}
// retrieve the image URL
let privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateManagedObjectContext.parentContext = coreDataStack.managedObjectContext
privateManagedObjectContext.performBlock {
NSURLSession.sharedSession().dataTaskWithURL(categoryIconURL) { (data, response, error) in
guard let httpUrlResponse = response as? NSHTTPURLResponse where httpUrlResponse.statusCode == 200,
let mimeType = response?.MIMEType where mimeType.hasPrefix("image"),
let data = data
where error == nil
else {
print("ERROR: Problem downloading the category icon.")
print(error)
return
}
// determine where to save the images
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
// retrieve the image name from the image address
let iconName = categoryIconAddress.componentsSeparatedByString("/").last!
// prepare the path for the image to be saved
let iconSaveDestinationPath = documentsPath.stringByAppendingString("/" + iconName)
// write the image to the prepared path
do {
try UIImageJPEGRepresentation(UIImage(data: data)!, 1.0)?.writeToFile(iconSaveDestinationPath, options: .DataWritingAtomic)
} catch {
print("There was a problem writing the downloaded image to the disk.")
print(error)
}
managedObject?.icon = iconSaveDestinationPath
}.resume()
if privateManagedObjectContext.hasChanges {
do {
try privateManagedObjectContext.save()
} catch {
print("An error occured while saving the child context: \(error)")
}
}
coreDataStack.saveContext()
}
}

Related

Core Data- Using Multiple Merge Policies

I am importing a large amount of data and persisting it via CoreData, setting constraints to ensure no data is duplicated (e.g. if it is imported multiple times). I do this via batch insert and use the merge policy NSMergeByPropertyStoreTrumpMergePolicy because any existing data in the store should not be overwritten upon import.
Later, the user can trigger some actions that require another batch insert, but with the merge policy NSMergeByPropertyObjectTrumpMergePolicy because now I want any changed attributes to have the new values persisted to the store.
This is all just as I need, except... based on my experimentation, there is no way to effectively CHANGE the merge policy once you have set it.
Once we make an insert using NSMergeByPropertyStoreTrumpMergePolicy, even if we now update context.mergePolicy to NSMergeByPropertyObjectTrumpMergePolicy, this will have no effect. Core Data will continue to generate SQL for the prior merge policy instead. Restarting the app and running the two steps in the opposite order verifies the same behavior -- whichever merge policy you specify first will become "stuck" in Core Data's behavior, even if you later update context.mergePolicy again in your code.
In fact, I found that even if I set two different merge policies on two different contexts (e.g. viewContext and backgroundContext) -- the behavior still gets "stuck". Whichever merge policy you use first will become the merge policy Core Data is using to generate SQL requests, even for the other context!
Can anybody verify if they have run into this and thoughts on workarounds?
I have found one thing -- which is to go all the way up the stack and create a second PersistenceController (and with it, NSPersistentContainer), using one for each merge policy. This works, but seems like overkill / not as intended by Apple?
Here is a simple SwiftUI-based project for testing.
You just need a CoreData model with an entity named Item, and two String attributes: col1 and col2. Set col1 as a constraint on Item.
And I recommend adding the launch argument to your scheme -com.apple.CoreData.SQLDebug 1 so you can see the SQL that Core Data is generating. More info on that here: https://useyourloaf.com/blog/debugging-core-data/
import SwiftUI
import CoreData
class Model: ObservableObject {
var dataArray = [[String:Any]]()
}
struct ContentView: View {
#State var model: Model = Model()
var body: some View {
VStack {
Button("Insert Data - PropertyStoreTrump") {
insertData()
}
Button("Insert More Data - PropertyObjectTrump") {
insertMoreData()
}
Spacer()
Button("Fetch Data") {
fetchAllData()
}
Button("Clear Data") {
clearAllData()
}
}
}
// Insert data using NSMergeByPropertyStoreTrumpMergePolicy
private func insertData() {
model.dataArray = [[String:Any]]()
model.dataArray.append(["col1":"1", "col2":"1"])
model.dataArray.append(["col1":"2", "col2":"1"])
model.dataArray.append(["col1":"3", "col2":"1"])
batchInsert(context: PersistenceController.shared.container.viewContext, mergePolicy: NSMergeByPropertyStoreTrumpMergePolicy)
}
// Insert data using NSMergeByPropertyObjectTrumpMergePolicy
private func insertMoreData() {
model.dataArray = [[String:Any]]()
model.dataArray.append(["col1":"2", "col2":"0"])
model.dataArray.append(["col1":"3", "col2":"0"])
model.dataArray.append(["col1":"4", "col2":"0"])
batchInsert(context: PersistenceController.shared.container.newBackgroundContext(), mergePolicy: NSMergeByPropertyObjectTrumpMergePolicy)
}
private func batchInsert(context: NSManagedObjectContext, mergePolicy: AnyObject) {
context.mergePolicy = mergePolicy
context.perform {
let insertRequest = NSBatchInsertRequest(entity: Item.entity(), objects: model.dataArray)
do {
let result = try context.execute(insertRequest)
} catch {
print("batch insert error: \(error)")
}
}
}
private func fetchAllData() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Item")
do {
let results = try PersistenceController.shared.container.viewContext.fetch(fetchRequest) as! [Item]
for result in results {
print(" \(result.col1 ?? "") \(result.col2 ?? "")")
}
} catch {
print("error with fetch: \(error)")
}
}
private func clearAllData() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Item")
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try PersistenceController.shared.container.viewContext.execute(batchDeleteRequest)
} catch {
print("error with delete request: \(error)")
}
}
}
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CoreDataStackOverflow")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
The workaround I found is to add this line to PersistenceController
static let shared2 = PersistenceController()
then use PersistenceController.shared.container... for one merge policy
and PersistenceController.shared2.container... for the other

Multiple duplicate NSPersistentStoreRemoteChange notifications fired from CloudKit + CoreData

I am seeing .NSPersistentStoreRemoteChange notifications being received multiple times, sometimes up to 10 times.
I don't think this is harmful, but best case it's using up processing power.
So my questions are:
Is this harmful?
Can I prevent it?
If not, is there a recommended way to ignore duplicate notifications?
--
I have the following code to setup my container. This is contained in the initialiser of a singleton and I have confirmed that it is called once.
guard let modelURL = Bundle(for: type(of: self)).url(forResource: momdName, withExtension:"momd"),
let mom = NSManagedObjectModel(contentsOf: modelURL)
else {
fatalError("🔐 Error loading model from bundle")
}
let containerURL = folderToStoreDatabaseIn.appendingPathComponent("Model.sqlite")
container = NSPersistentCloudKitContainer(name: momdName, managedObjectModel: mom)
guard let description = container.persistentStoreDescriptions.first else {
fatalError("🔐 ###\(#function): Failed to retrieve a persistent store description.")
}
description.url = containerURL
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
super.init()
// this must be called after super.init()
// ***** ADD OBSERVER *****
NotificationCenter.default.addObserver(self,
selector: #selector(updatedFromCKCD(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator)
if let tokenData = try? Data(contentsOf: tokenFile) {
do {
lastToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
Logger.error("🔐 ###\(#function): Failed to unarchive NSPersistentHistoryToken. Error = \(error)")
}
}
The code to process these changes:
// https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes
#objc func updatedFromCKCD(_ notifiction: Notification) {
let fetchHistoryRequest = NSPersistentHistoryChangeRequest.fetchHistory(
after: lastToken
)
let context = container.newBackgroundContext()
guard
let historyResult = try? context.execute(fetchHistoryRequest)
as? NSPersistentHistoryResult,
let history = historyResult.result as? [NSPersistentHistoryTransaction]
else {
Logger.error("⛈ Could not convert history result to transactions")
assertionFailure()
return
}
Logger.debug("⛈ Found cloud changes since: \(self.lastToken?.description ?? "nil")")
DispatchQueue.main.async {
// look for particular set of changes which require the user to take action
...
}
}

NSFetchedResultsController can't update the tableView swift4

I'm trying to make it work for last couple of days and can't get it working. Its something tiny detail obviously I can't seem to find.
Could you take a look and give me some insights about my code?
I'm trying to update the logView with app savings in the coredata.
Here's the entire code for ViewController and CoreData Handler.
/// fetch controller
lazy var fetchController: NSFetchedResultsController = { () -> NSFetchedResultsController<NSFetchRequestResult> in
let entity = NSEntityDescription.entity(forEntityName: "Logs", in: CoreDataHandler.sharedInstance.backgroundManagedObjectContext)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>()
fetchRequest.entity = entity
let nameDescriptor = NSSortDescriptor(key: "name", ascending: false)
fetchRequest.sortDescriptors = [nameDescriptor]
let fetchedController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataHandler.sharedInstance.backgroundManagedObjectContext, sectionNameKeyPath: "duration", cacheName: nil)
fetchedController.delegate = self as? NSFetchedResultsControllerDelegate
return fetchedController
}()
override func viewDidLoad() {
title = "Week Log"
tableView.tableFooterView = UIView(frame: CGRect.zero)
tableView.separatorColor = UIColor.black
tableView.backgroundColor = UIColor.red
refreshView()
loadNormalState()
loadCoreDataEntities()
}
/**
Refresh the view, reload the tableView.
*/
func refreshView() {
loadCoreDataEntities()
tableView.reloadData()
}
/**
Load history entities from core data. (I'm printing on the console and
be able to see the the fetched data but I can't load it to tableView.)
*/
func loadCoreDataEntities() {
do {
try fetchController.performFetch()
} catch {
print("Error occurred while fetching")
}
}
import Foundation
import CoreData
class CoreDataHandler: NSObject {
/**
Creates a singleton object to be used across the whole app easier
- returns: CoreDataHandler
*/
class var sharedInstance: CoreDataHandler {
struct Static {
static var instance: CoreDataHandler = CoreDataHandler()
}
return Static.instance
}
lazy var backgroundManagedObjectContext: NSManagedObjectContext = {
let backgroundManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let coordinator = self.persistentStoreCoordinator
backgroundManagedObjectContext.persistentStoreCoordinator = coordinator
return backgroundManagedObjectContext
}()
lazy var objectModel: NSManagedObjectModel = {
let modelPath = Bundle.main.url(forResource: "Model", withExtension: "momd")
let objectModel = NSManagedObjectModel(contentsOf: modelPath!)
return objectModel!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.objectModel)
// Get the paths to the SQLite file
let storeURL = self.applicationDocumentsDirectory().appendingPathComponent("Model.sqlite")
// Define the Core Data version migration options
let options = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]
// Attempt to load the persistent store
var error: NSError?
var failureReason = "There was an error creating or loading the application's saved data."
do {
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject
dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() 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.
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return persistentStoreCoordinator
}()
func applicationDocumentsDirectory() -> NSURL {
return FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).last! as NSURL
}
func saveContext() {
do {
try backgroundManagedObjectContext.save()
} catch {
print("Error while saving the object context")
// Error occured while deleting objects
}
}
You have a data source delegate somewhere. That data source delegate tells the table view how many items there are, and what their contents is. How does it know how many items? That must be stored somewhere.
When the fetch controller is successful, it must modify the data that the data source delegate relies on in some way, and then call reloadData. Are you doing this? Are you doing anything that causes the data source delegate to change the number of items it reports?
And calling loadCoreDataEntities, immediately followed by reloadData, is nonsense. loadCoreDataEntities is asynchronous. By the time you call reloadData, it hasn't loaded any entities yet. realodData is called when loadCoreDataEntities has finished.

doubled savings with core data swift 4

developing a little app for my comic collection encountered this issue:
in my second "add comic" VC I have a button and the func below, but I save TWICE entities in manged context (ate least, I think this is the issue)
for example if I have 2 comics yet shown in main VC tableview, go to "add comic VC" and save a third one, going back to main VC I'll print 3 objects with title, number etc but also print 2 new objects with no data as I had saved twice a manger context a "right one" and another one with same number of object but empty. If I keep adding a 4th comic, I'll get 6 complete comic + the 4th and more 6 "blank itmes" with default values "no title"
let kComicEntityName = "Comic"
func addingSingleComic(gotTitle: String, gotIssue: Int16, gotInCollection: Bool ) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
let managedContext = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: kComicEntityName, in: managedContext)!
let comicToAdd = Comic(entity: entity, insertInto: managedContext)
comicToAdd.comicTitle = gotTitle
comicToAdd.issueNumber = gotIssue
comicToAdd.inCollection = gotInCollection
do {
try managedContext.save()
} catch let error as NSError {
print("could not save. \(error), \(error.userInfo)")
}
print("new single comic crated: title: \(comicToAdd.comicTitle ?? "!! not title !!"), n. \(comicToAdd.issueNumber), owned?: \(comicToAdd.inCollection)")
}
in the main VC I use this to check items in core data
func asyncPrintEntities() {
self.asyncComicEntityArray.removeAll(keepingCapacity: true)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
let managedContext = appDelegate.persistentContainer.viewContext
let comicFetch : NSFetchRequest<Comic> = Comic.fetchRequest()
asyncFetchRequest = NSAsynchronousFetchRequest<Comic>(fetchRequest: comicFetch) {
[unowned self] (result: NSAsynchronousFetchResult) in
guard let AllComicEntityResult = result.finalResult else {
return
}
self.asyncComicEntityArray = AllComicEntityResult
//************************************
do {
self.asyncComicEntityArray = try managedContext.fetch(comicFetch)
if self.asyncComicEntityArray.count > 0 {
print("Ok! model is not empty!")
} else {
print("No entites availabe")
}
} catch let error as NSError {
print("Fetch error: \(error) description: \(error.userInfo)")
}
guard self.asyncComicEntityArray != nil else {return}
for comicFoundInArray in self.asyncComicEntityArray {
let entity = NSEntityDescription.entity(forEntityName: self.kComicEntityName, in: managedContext)!
var comicTouse = Comic(entity: entity, insertInto: managedContext)
// var comicTouse = Comic() //to be deleted since this kind of init is not allowed, better above insertInto
comicTouse = comicFoundInArray as! Comic
print("comic title: \(comicTouse.comicTitle ?? "error title"), is it in collection? : \(comicTouse.inCollection)")
}
self.MyTableView.reloadData()
//************************************
}
// MARK: - async fetch request 3
do {
try managedContext.execute(asyncFetchRequest)
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
//end of function
}
In your addingSingleComic you create a new Comic here:
let comicToAdd = Comic(entity: entity, insertInto: managedContext)
Then you assign values to the object's properties.
Separately, in asyncPrintEntities, you also create new Comic objects here:
var comicTouse = Comic(entity: entity, insertInto: managedContext)
This time you do not assign values to the object's properties. They will have no title, etc, because you created them but never assigned a title. This line executes once for every object in asyncComicEntityArray, so if the array has two objects, you create two new objects that contain no data. You don't use comicToUse anywhere except in the one print, but it still exists in the managed object context and will still get saved the next time you save changes.
This is why you're getting extra entries-- because you're creating them in this line of code. It's not clear why you're creating them here. You just executed a fetch request, and then you immediately create a bunch of no-data entries which you don't use. It looks like that entire for loop could just be deleted, because the only thing it does is create these extra entries.

Deleting all data in a Core Data entity in Swift 3

Is there a way to do a batch delete of all data stored in all of the entities in core data?
I read somewhere that in iOS 9 or 10 that apple introduced a way to do batch deletes, but I can't seem to find any good information on it.
Ultimately, I just need a function that goes through an entity, and deletes all of the data in it. Seems like it should be simple enough, but documentation/tutorials for it have proven exceedingly difficult to find.
Any thoughts?
Edit
I added the following code into an IBAction attached to a button:
#IBAction func clearAllData(_ sender: AnyObject) {
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "PLProjects")
let request = NSBatchDeleteRequest(fetchRequest: fetch)
//get the data from core data
getPLData()
//reload the table view
tableView.reloadData()
}
This does not seem to work however. If I close down the project and reopen it, the data is still there. I am assuming this is also why the table view doesn't update, because the data is not actually being deleted.
You're thinking of NSBatchDeleteRequest, which was added in iOS 9. Create one like this:
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
let request = NSBatchDeleteRequest(fetchRequest: fetch)
You can also add a predicate if you only wanted to delete instances that match the predicate. To run the request:
let result = try managedObjectContext.executeRequest(request)
Note that batch requests don't update any of your current app state. If you have managed objects in memory that would be affected by the delete, you need to stop using them immediately.
To flesh out Tom's reply, this is what I added to have a complete routine:
func deleteAllRecords() {
let delegate = UIApplication.shared.delegate as! AppDelegate
let context = delegate.persistentContainer.viewContext
let deleteFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "CurrentCourse")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: deleteFetch)
do {
try context.execute(deleteRequest)
try context.save()
} catch {
print ("There was an error")
}
}
Declare the Method for getting the Context in your CoreDataManager
Class
class func getContext() -> NSManagedObjectContext {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
}
if #available(iOS 10.0, *) {
return appDelegate.persistentContainer.viewContext
} else {
return appDelegate.managedObjectContext
}
}
Call the above method from your NSManagedObject subClass:
class func deleteAllRecords() {
//getting context from your Core Data Manager Class
let managedContext = CoreDataManager.getContext()
let deleteFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Your entity name")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: deleteFetch)
do {
try managedContext.execute(deleteRequest)
try managedContext.save()
} catch {
print ("There is an error in deleting records")
}
}

Resources