Custom entity migration in swift coredata not working - ios

I changed the type of one of the fields in an entity i created in iOS from a dictionary to a list and created a new version for that entity which is set as default but kept the same name, but migration is not working as whenever i try to iterate that field in a deserialized object it appears to still be a dictionary due to unrecognized selector error for indexAt i receive.
I created a subclass of NSEntityMigrationPolicy to migrate a field from a dictionary to string type ([String : TimerData] -> [TimerData]) with a method like so:
class SessionTransformationPolicy : NSEntityMigrationPolicy {
#objc func transformTimerRows(data: [String : TimerData]?) -> [TimerData] {
return Array(data!.values).map { orig in
TimerData(old: orig)
}
}
}
I created a mapping model and set it to this migration policy, but whenever i put breakpoints or print statements in the migration policy it doesnt get executed, i made sure to specify in the model mapping the method i created based on online sources:
FUNCTION($entityPolicy, "transformTimerRowsWithData:" , $source.timerRows)
I've done a bunch of research into this issue and the only solution i've found was to go into the app delegate and change shouldInferMappingModelAutomatically for the container to false but this just results in no data being loaded and the migration policy still not being called, can someone tell me what i'm doing wrong?
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Model")
let description = NSPersistentStoreDescription()
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = false
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
else {
}
})
return container
}()

Related

An error occurred during persistent store migration (Cannot migrate store in-place) Core Data

I added new model version and select it like default one. Then I added new attribute to existing entity in new model version. To test migration I installed previous version of app and add filled with all data in app. Then I install new changes and it work on simulator. If I move that build to TestFlight and test via real device then I get this error:
Fatal error: ###persistentContainer: Failed to load persistent stores:Error Domain=NSCocoaErrorDomain Code=134110 "An error occurred during persistent store migration." UserInfo={sourceURL=file:///private/var/mobile/Containers/Shared/AppGroup/DBB80C75-1B07-4318-8BA3-3F4FFC14FBD7/AppName.sqlite, reason=Cannot migrate store in-place: near "null": syntax error, destinationURL=file:///private/var/mobile/Containers/Shared/AppGroup/DBB80C75-1B07-4318-8BA3-3F4FFC14FBD7/AppName.sqlite, NSUnderlyingError=0x281958180 {Error Domain=NSCocoaErrorDomain Code=134110 "An error occurred during persistent store migration." UserInfo={reason=near "null": syntax error, NSSQLiteErrorDomain=1, NSUnderlyingException=near "null": syntax error}}}
I need to use CoreData for Widget's target as well. Code is here :
final class CoreDataStack {
static let shared = CoreDataStack()
var context: NSManagedObjectContext { persistentContainer.viewContext }
var container: NSPersistentContainer { persistentContainer }
private let containerName = "AppName"
private var persistentContainer: NSPersistentContainer!
// MARK: - Setup
func setup() {
guard let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "app_group")?.appendingPathComponent(containerName + ".sqlite") else {
fatalError("Error finding Model from File Manager")
}
let container = NSPersistentContainer(name: containerName)
let description = NSPersistentStoreDescription(url: storeURL)
description.type = NSSQLiteStoreType
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
container.persistentStoreDescriptions = [description]
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
let crashlitycService: CrashlitycUseCase = CrashlitycService()
crashlitycService.record(error: .errorForAnalytic(error), file: #file, function: #function, line: #line)
fatalError("###\(#function): Failed to load persistent stores:\(error)")
}
})
self.persistentContainer = container
}
}
iOS 16.2 Xcode 14.1
Similar issues I found here, but without any success:
An error occurring during Core Data persistent store migration in iOS 13
iOS app crashing on launch screen due to core data migration
Please help me to figure out how can I do this migration?

How can I make multiple calls of NSBatchUpdateRequest within DB transaction so that either all rows is updated or none is updated?

Is there a way, to make multiple NSBatchUpdateRequest calls executed within a DB transaction, so that either all DB rows is updated or none is updated (When exception thrown)?
The following code illustrate the problem.
func debug() {
let coreDataStack = CoreDataStack.INSTANCE
let backgroundContext = coreDataStack.backgroundContext
backgroundContext.perform {
let fetchRequest = NSTabInfo.fetchSortedRequest()
do {
var objectIDs: [NSManagedObjectID] = []
let nsTabInfos = try fetchRequest.execute()
//
// QUESTION: We are updating multiple rows of data directly in a persistent store.
// How can we ensure either all rows is updated, or none row is updated is exception
// happens in between?
//
for nsTabInfo in nsTabInfos {
let batchUpdateRequest = NSBatchUpdateRequest(entityName: "NSTabInfo")
batchUpdateRequest.predicate = NSPredicate(format: "self == %#", nsTabInfo.objectID)
batchUpdateRequest.propertiesToUpdate = ["name": nsTabInfo.name! + "XXX"]
batchUpdateRequest.resultType = .updatedObjectIDsResultType
let batchUpdateResult = try backgroundContext.execute(batchUpdateRequest) as? NSBatchUpdateResult
guard let batchUpdateResultX = batchUpdateResult else { return }
guard let managedObjectIDs = batchUpdateResultX.result else { return }
if let nsManagedObjectIDs = managedObjectIDs as? [NSManagedObjectID] {
objectIDs.append(contentsOf: nsManagedObjectIDs)
}
//
// Simulate some exception
// We notice the first row is updated & rest of the rows are unchanged.
// This leaves our data in inconsistent state.
//
throw "Custom error!!!"
}
if !objectIDs.isEmpty {
let changes = [NSUpdatedObjectsKey : objectIDs]
coreDataStack.mergeChanges(changes)
}
} catch {
backgroundContext.rollback()
error_log(error)
}
}
}
class CoreDataStack {
static let INSTANCE = CoreDataStack()
private init() {
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "wenote")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
private(set) lazy var backgroundContext: NSManagedObjectContext = {
let backgroundContext = persistentContainer.newBackgroundContext()
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return backgroundContext
}()
func mergeChanges(_ changes: [AnyHashable : Any]) {
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: changes,
into: [persistentContainer.viewContext, backgroundContext]
)
}
}
We write a demo code to illustrate the following
Performing NSBatchUpdateRequest multiple times within a loop.
An exception happens in between.
We wishes none of the row in persistent store is updated. However, a row is already updated before the exception thrown.
May I know what technique I can use, which is similar to SQLite transaction feature, so that either all rows is updated, or none of the row is updated when exception happens?
CoreData.framework doesn't open up SQLite level controls to the user, it provides you NSManagedObjectContext.
How does it work in a similar manner?
You pull as many objects in many as you need and do your changes on them.
When you are done with your changes, you do context.save().
In that way, you save all of your changes in one shot.
In all cases, pulling all objects in memory might not be possible or a good idea, so then you need to implement your own solution around how to send all of these changes to disk.
From the NSBatchUpdateRequest docs -
A request to Core Data to do a batch update of data in a persistent store without loading any data into memory.
When you execute this, you are doing the changes in store that you can't roll back. For a large data-set, you can do following -
Say you have to perform a series of updates (5 different steps) on 100k records as an operation.
Start in a background thread, pull objects in memory in batches of 1k at a time.
You can load 1k objects easily in memory, mutate them - go through all of your changes/steps one by one and save these changes on this batch. If this is successful, you move on to the next batch.
In case one intermediate step fails on a batch, you can then use either NSManagedObjectContext.rollback() or NSManagedObjectContext.reset() depending on your implementation.
Here's a popular SO post on the differences between the two in case official docs don't provide enough clarity.

Cloudkit - Dashboard - There are no "Entity" records in this database

I'm trying to store some data in Cloudkit by Xcode but when I push QueryRecords in the Dashboard "There are no "Entity" records in this database" is displayed but my "SaveFunction" reports a "Saved successfully". Did I miss something ? I'm very stuck.... Thank you for any help you can offer!...
1) Signing & Capabilities Part :
Key-value storage checked
CloudKit checked
Containers iCloud.com.myusername.nameOfTheProject
2) Xcdatamodeld
"Entity" contains 5 attributes, the first entity has a Default Value
3) AppDelegate Part
import UIKit
import CoreData
[...]
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "NameOfContainer")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
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 {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
4) ViewController
import UIKit
import CloudKit
class ViewController: UIViewController {
let myContainer = CKContainer(identifier: "iCloud.com.MyUserName.NameOfTheProject")
override func viewDidLoad() {
super.viewDidLoad()
let artworkRecord = CKRecord(recordType: "Entity")
artworkRecord["customFieldName"] = "xxxx" as NSString
// Do any additional setup after loading the view.
let privateDatabase = myContainer.privateCloudDatabase
privateDatabase.save(artworkRecord) { (record, error) in
if let error = error {
print(error)
// Insert error handling
return
}
print("Saved successfully")
// Insert successfully saved record code
}
}
}
5) CloudKit Dashboard
The Container selected is the good one
Database : Private Database with my same Id used in Xcode
Zone : _DefaultZone
Custom field "customFieldName" displayed ("queryable, searchable,
sortable") as xcdatamodeld
"recordName" field created with an Index Type QUERYABLE
Please, check if your development account and your iCloud account on your test device are the same. I also have the problem, when I test iCloud access on a test device, that has a different apple-id then my development apple-id.
Like the Apple documentation in the section "Select or Create an iCloud Account for Development" says: "Note that your iCloud account is distinct from your Apple Developer account; however, you can use the same email address for both. Doing so gives you access to your iCloud account’s private user data in CloudKit Dashboard, which can be helpful for debugging."
You are using the private database and if you then want to see the private records, your test device must have the same apple-id as your development account id.
Kind regards,
MacUserT

Setting persistentStoreDescriptions causes no CoreData objects to be saved

I am trying to ensure that all of our CoreData is protected using Data Protection. When I try to set a NSPersistentStoreDescription on my container, no CoreData objects are saved. If I comment out the line indicated below, all objects are saved (and read) just fine. If I enable the line, nothing gets saved (or perhaps the read silently fails?). There are no errors generated and there are no logs generated. I do have the Data Protection entitlement in my provisioning profile (matching completeUnlessOpen). I've gotta be missing something very basic.
This is Xcode 8.2.1 (8C1002)
Can anyone offer any insight / advice?
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "my_app")
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
description.setOption(FileProtectionType.completeUnlessOpen as NSObject?, forKey: NSPersistentStoreFileProtectionKey)
// *** ALLOWING THIS NEXT LINE TO EXECUTE CAUSES PROBLEM ***
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
print("Unresolved error \(error), \(error.userInfo)")
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
I understand that this question has been asked long time back, but I'm still going to post an answer so that anyone stuck at this problem gets some help.
In the above code snippet you posted in the question, you did not initialize NSPersistentStoreDescription with SQLITE file URL.
So, it does not know what persistent container it represents. Please refer below working code.
let container = NSPersistentContainer(name: "AppName")
if let storeDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
let sqliteURL = storeDirectory.appendingPathComponent("AppName.sqlite")
//Set Protection for Core data sql file
let description = NSPersistentStoreDescription(url: sqliteURL)
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
description.setOption(FileProtectionType.complete as NSObject, forKey: NSPersistentStoreFileProtectionKey)
container.persistentStoreDescriptions = [description]
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in...}
Let me know if this works for you and please accept the answer if it does, so that others will know.
Your error is actually because you're setting options that should relate to Core Data. File protection is done under FileManager setAttributes function calls.
However, the answer to the underlying question is that you don't need to add file protection to individual files - they're set to the default protection you declare in your entitlement.
This can be verified by printing the output of:
let attributes = try FileManager.default.attributesOfItem(atPath: url.relativePath)

Managed Object Context as Singleton?

I am facing issues with my app where if i create or delete a new object, then save the object within a different entity object, then go back and try to make a new object of the first entity type, my app will crash.
I can then reopen then app and make the object that crashed the app with no issue.
This is all being done via core data, there is an exercise, exercises are saved as a routine, then creating a new exercise after having created a routine will crash the app. Furthermore, deleting an exercise and a routine then trying to create a new one straight after will also crash the app
I have spend a long time reading around this and believe the likely cause is managed object context and wondered if creating it as a singleton was the solution? I set up the MoC by running the below in each VC's viewdidload:
func getMainContext() -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
I then reference this VC level variable via .self wherever i need to reference the MoC to avoid clashes with creating further MoC within a VC.
I believed this should prevent issues as all core data work is linked to the shared MoC. However as documented above, there are still crashes occurring.
Below is a console print of the crash which hopefully will narrow down the source.
fatal error: Failure to save context: Error Domain=NSCocoaErrorDomain Code=134020 "(null)" UserInfo={NSAffectedObjectsErrorKey= (entity: UserExercise; id: 0x600000025060
The code block this is triggering off as an example as 1 location in the app it occurs is included below, to clarify this only occurs when i just deleted other objects, if i reloaded the app now this code would work and save just fine:
func createExercise() {
print("SAVE EXERCISE PRESSED")
if userExercise == nil {
print("SAVING THE NEW EXERCISE")
let newUserExercise = UserExercise(context: self.managedObjectContext!)
newUserExercise.name = userExerciseName.text
newUserExercise.sets = Int64(userSetsCount)
newUserExercise.reps = Int64(userRepsCount)
newUserExercise.dateCreated = NSDate()
newUserExercise.hasBeenTickedDone = false
} if self.associatedRoutineToAddTo != nil {
let fetchRequest: NSFetchRequest<UserRoutine> = UserRoutine.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name == %#", self.associatedRoutineToAddTo!)
do {
let existingUserRoutine = try self.managedObjectContext!.fetch(fetchRequest).first
print("RETRIVED ROUTINES ARRAY CONTAINING \(existingUserRoutine)")
existingUserRoutine?.addToUserexercises(newUserExercise)
print("EXERCISE SUCESSFULLY ADDED TO ROUTINE")
} catch {
print("Fetching Routine Failed")
}
} else if self.associatedRoutineToAddTo == nil {
print("THIS IS A FRESH EXERCISE WITHOUT A PARENT ROUTINE")
}
} else if let userExercise = userExercise {
print("UPDATING THE EXISTING EXERCISE")
userExercise.name = userExerciseName.text
userExercise.sets = Int64(userSetsCount)
userExercise.reps = Int64(userRepsCount)
}
do {
try self.managedObjectContext?.save()
print("THE EXERCISE HAS BEEN SAVED")
} catch {
fatalError("Failure to save context: \(error)")
}
The variable declarations are:
var managedObjectContext: NSManagedObjectContext!
var userExercise: UserExercise?
var associatedRoutineToAddTo : String?
var editingUserExerciseID: NSManagedObjectID?
var editingUserExercise: UserExercise?
I was receiving the "NSCocoaErrorDomain Code=134020 (null)" error because my new entity was not added to the proper CoreData Configuration.

Resources