I've tested my code with 3 x iPhone 5's, a 5s, 6, 6s, and 7.
I'm getting the above error on all of the iPhone 5 devices only. No idea what is going on here but perhaps the fact that the 5's are 32bit devices may be a clue?
I'm calling the following method from a viewcontroller class
func startRecording() {
disableControls()
CoreDataStack.shared.performForegroundTask { (context) in
let sessionInfo = SessionInfo(context: context)
sessionInfo.startTime = Date().timeIntervalSince1970
sessionInfo.userId = self.config.userId
sessionInfo.devicePosition = self.config.devicePosition.rawValue
sessionInfo.deviceType = self.config.deviceType.rawValue
sessionInfo.deviceNumber = self.config.deviceNumber
sessionInfo.deviceSide = self.config.deviceSide.rawValue
do {
try context.obtainPermanentIDs(for: [sessionInfo])
} catch {
print("Error obtaining permanent ID for session info record")
return
}
CoreDataStack.shared.saveViewContextAndWait()
DispatchQueue.main.async {
guard sessionInfo.objectID.isTemporaryID == false else {
print("ObjectID is temporary")
return
}
self.recording = true
self.statusLabel.text = "Recording..."
self.recordManager.start(sessionUID: sessionInfo.uid)
}
}
}
The config variable is a simple struct:
struct Configuration {
var userId: String = "Unknown"
var deviceType: DeviceType = .phone // enum: String
var deviceSide: DeviceSide = .notApplicable // enum: String
var deviceNumber: Int16 = 1
var devicePosition: DevicePosition = .waist // enum: String
}
The CoreDataStack is here:
final class CoreDataStack {
static let shared = CoreDataStack()
private init() {}
var errorHandler: (Error) -> Void = { error in
log.error("\(error), \(error._userInfo)")
}
private struct constants {
static let persistentStoreName = "Model"
}
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: constants.persistentStoreName)
container.loadPersistentStores(completionHandler: { [weak self] (storeDescription, error) in
if let error = error {
self?.errorHandler(error)
}
})
return container
}()
lazy var viewContext: NSManagedObjectContext = {
self.persistentContainer.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
self.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
try! self.persistentContainer.viewContext.setQueryGenerationFrom(.current)
return self.persistentContainer.viewContext
}()
private lazy var backgroundContext: NSManagedObjectContext = {
let context = self.persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
return context
}()
func performForegroundTask(_ block: #escaping (NSManagedObjectContext) -> Void) {
self.viewContext.performAndWait {
block(self.viewContext)
}
}
func performBackgroundTask(_ block: #escaping (NSManagedObjectContext) -> Void) {
backgroundContext.perform {
block(self.backgroundContext)
}
}
func saveBackgroundContext() {
viewContext.performAndWait {
do {
if self.viewContext.hasChanges {
try self.viewContext.save()
}
} catch {
self.errorHandler(error)
}
self.backgroundContext.perform {
do {
if self.backgroundContext.hasChanges {
try self.backgroundContext.save()
self.backgroundContext.refreshAllObjects()
}
} catch {
self.errorHandler(error)
}
}
}
}
func saveViewContext() {
viewContext.perform {
if self.viewContext.hasChanges {
do {
try self.viewContext.save()
} catch {
self.errorHandler(error)
}
}
}
}
func saveViewContextAndWait() {
viewContext.performAndWait {
if self.viewContext.hasChanges {
do {
try self.viewContext.save()
} catch {
self.errorHandler(error)
}
}
}
}
}
The code is bombing out on the following line in the startRecording method:
try context.obtainPermanentIDs(for: [sessionInfo])
Edit:
I've created a stripped down test application consisting of only the CoreDataStack and a model with one entity with one attribute of type string. I'm still getting the same error on 3x iPhone 5's only. 5s, 6, 6s, 7 all work fine.
It would imply the problem lies with the CoreDataStack perhaps?
Github repo here
A couple of people have asked if I solved this, so here is what I did. This is not really a solution but more of a workaround. It may not be appropriate for everyone but worked for me.
All I did was remove the line
try! self.persistentContainer.viewContext.setQueryGenerationFrom(.current)
from the CoreDataStack and the issue went away...
I'll take a couple of guesses based on a short look at your code.
The old-school developer in me is drawn to the Configuration member var deviceNumber: Int16 = 1. Incorrect alignment settings or old compilers might cause the next item to have the wrong alignment. You could try making it the last item in the struct.
Another item which stands out is the assignment sessionInfo.deviceNumber = self.config.deviceNumber. This looks to be assigning an Int16 to an NSNumber which may be a problem. (I am assuming that SessionInfo is an NSManagedObject based on the overall code and its initializer taking a context argument. That would mean all numeric members are NSNumbers.)
Try changing the line to
sessionInfo.deviceNumber = NSNumber(int:self.config.deviceNumber)
I'm not yet familiar with the iOS 10 additions to Core Data, but from what I'm reading the viewContext is read-only. But viewContext is being used to create a new object in this code. Is the Xcode console showing any more information when you get to this point in the debugger?
NSPersistentContainer is a setup for a core data stack with clear expectation on how to use it. You are misusing it. viewContext is readonly so remove performForegroundTask, saveViewContext and saveViewContextAndWait. Don't change the viewContext's mergePolicy. In performBackgroundTask just use NSPersistentContainer's performBackgroundTask which has the same method signature. Don't use newBackgroundContext. If you are using NSPersistentContainer your CoreDataStack should be doing next to nothing.
If you want a different custom stack unlike what NSPersistentContainer sets up then don't use NSPersistentContainer - just create your own stack. But the setup that you are trying to write has major problems. Writing from both background contexts and the viewContext has major problems when writes happen at the same time. mergePolicy can help that but you can end up missing information that you thought you saved. You are much better off learning to use the stack that NSPersistentContainer sets up.
Related
I am refactoring my existing core data manager to use an NSPersistentContainer. I have what seems to be some relatively straightforward code to fetch my entities:
class CoreDataManager {
static let shared: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DateAid")
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { storeDescription, error in
if let error = error {
print(error.localizedDescription)
}
}
return container
}()
static func fetch<T: NSManagedObject>() throws -> [T] {
let context = shared.viewContext
let request = NSFetchRequest<T>(entityName: String(describing: T.self))
do {
return try context.fetch(request) as [T]
} catch {
throw error
}
}
}
However, in another class where I call this code, it retrieves nothing. I am calling it like so:
do {
let events: [Event] = try CoreDataManager.fetch()
} catch {
throw error
}
It's not erroring out, but it's not retrieving anything. Hence, I can't get any idea what's going wrong. Things to note:
My xcdatamodel is correctly named "DateAid"
Breakpoints are hitting in the container's loadPersistentStores and I can print to the console a proper storeDescription that shows the path to the sqlite file
String(describing: T.self) works fine, as it returns "Event" (I've tried hard-coding the string "Event" as well to no avail)
the persistentContainer's properties all seem to be set.
I know the entities are all still there because when I go back to my existing implementation (with all the manual core data setup in the app delegate), it properly fetches the entities.
What could the issue be? Is there a chance it could be some kind of sync/async race condition issue happening outside of my example? Am I missing something obvious? Any help or direction to either solve or figure out how to debug the problem is greatly appreciated.
loadPersistentStores is never called in your implementation.
instead of doing this (which you never call)
private var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DateAid")
container.loadPersistentStores { storeDescription, error in
if let error = error {
print(error.localizedDescription)
}
}
return container
}()
create singleton instance of your NSPersistentContainer like this:
static let shared: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DateAid")
container.loadPersistentStores { storeDescription, error in
if let error = error {
print(error.localizedDescription)
}
}
return container
}()
Note:
static let shared = CoreDataManager()
Should be removed, because it is not doing anything at the moment.
Edit 1:
I just saw that you are loading persistent store each time you make fetch request, you should load the store only once because it is adding unnecessary work each time you perform a fetch request.
Edit 2:
You can make the CoreDataManager subclass of NSPersistentContainer and you can access the viewContext right within the class without shared.viewContext
Quote from Apple
NSPersistentContainer is intended to be subclassed. Your subclass is a
convenient place to put Core Data–related code like functions that
return subsets of data and calls to persist data to disk.
Edit 3 - How to:
Subclassing
class CoreDataManager: NSPersistentContainer {
private override init(name: String, managedObjectModel model: NSManagedObjectModel) {
super.init(name: name, managedObjectModel: model)
}
public static let shared: CoreDataManager = {
let container = CoreDataManager(name: "DateAid")
container.loadPersistentStores { storeDescription, error in
if let error = error {
print(error.localizedDescription)
}
}
try? container.viewContext.setQueryGenerationFrom(.current)
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
public func fetch<T: NSManagedObject>() throws -> [T] {
let request = NSFetchRequest<T>(entityName: String(describing: T.self))
do {
return try self.viewContext.fetch(request) as [T]
} catch {
throw error
}
}
}
Fetch request
do {
let events: [Event] = try CoreDataManager.shared.fetch()
} catch {
throw error
}
I want to give the user the option to toggle iCloud sync on and off.
After researching for a while, I saw that one way one could achieve this is by setting the cloudKitContainerOptions.
So I would set it to nil if I don't want my database to be synched.
if(!UserDefaultsManager.shared.iCloudSyncOn) {
description.cloudKitContainerOptions = nil
}
That's all working fine, but I haven't found a way to do that during runtime.
I have tried to reinitialize my container when the user toggles, so that my container has different cloudKitContainerOptions depending on the choice.
But this would only return me an error, when saving the context, saying: Thread 1: "Illegal attempt to establish a relationship 'addEntries' between objects in different contexts ..., which I believe is due to the reinitialization.
I think I would have to pass down the newly created context to my whole view hierarchy, anything that caches the moc?
Here would be a simplified snipped of my CoreDataStack:
func setupContainer() -> NSPersistentContainer {
let container = NSPersistentCloudKitContainer(name: "...")
guard let description = container.persistentStoreDescriptions.first else { ... }
...
if(!UserDefaultsManager.shared.iCloudSyncOn) {
description.cloudKitContainerOptions = nil
}
container.loadPersistentStores(completionHandler: { ... })
...
return container
}
When the user toggles, setupContainer() gets called.
Any help would be awesome, alternative ways are of course also welcomed!
Thanks.
I have been able to make it work!
My problem specifically was that I haven't updated my already fetched objects (with the old context) after reinitializing the persistenceContainer (which created a new context).
So, directly after calling setupContainer(), a simple fetch (with the new context) for all my objects was enough.
self.container = setupContainer()
CoreDataManager.shared.fetchAllItem()
Additionals
I have encountered one more problem due to the reinitialization, which was a warning that multiple NSEntityDescriptions were claiming NSManagedObject Subclass of my entities.
This answer fixed it for me.
Final Code
Maybe this could help you out. Works fine for me. (iOS 14.2)
Slightly modified.
PS: Instead of setting the cloudKitContainerOptions I ended up switching between NSPersistentCloudKitContainer and NSPersistenttContainer.
lazy var container: NSPersistentContainer = {
setupContainer()
}()
func updateContainer() {
saveContext()
container = setupContainer()
CoreDataManager.shared.fetchAllItems()
}
private func setupContainer() -> NSPersistentContainer {
let iCloud = UserDefaultsManager.shared.settingICloudSynch
do {
let newContainer = try PersistentContainer.getContainer(iCloud: iCloud)
guard let description = newContainer.persistentStoreDescriptions.first else { fatalError("No description found") }
if iCloud {
newContainer.viewContext.automaticallyMergesChangesFromParent = true
newContainer.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
} else {
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
newContainer.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") }
}
return newContainer
} catch {
print(error)
}
fatalError("Could not setup Container")
}
final class PersistentContainer {
private static var _model: NSManagedObjectModel?
private static func model(name: String) throws -> NSManagedObjectModel {
if _model == nil {
_model = try loadModel(name: name, bundle: Bundle.main)
}
return _model!
}
private static func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
throw CoreDataModelError.modelURLNotFound(forResourceName: name)
}
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
throw CoreDataModelError.modelLoadingFailed(forURL: modelURL)
}
return model
}
enum CoreDataModelError: Error {
case modelURLNotFound(forResourceName: String)
case modelLoadingFailed(forURL: URL)
}
public static func getContainer(iCloud: Bool) throws -> NSPersistentContainer {
let name = "YOUR APP"
if iCloud {
return NSPersistentCloudKitContainer(name: name, managedObjectModel: try model(name: name))
} else {
return NSPersistentContainer(name: name, managedObjectModel: try model(name: name))
}
}
}
I have CoreDataStack
I've added debug options for CoreData debugging
"-com.apple.CoreData.ConcurrencyDebug 1"
class CoreDataStack {
public enum SaveStatus {
case saved, rolledBack, hasNoChanges, error
}
private var modelName: String
var viewContext: NSManagedObjectContext
var privateContext: NSManagedObjectContext
var persisterContainer: NSPersistentContainer
init(_ modelName: String) {
self.modelName = modelName
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores { persisterStoreDescription, error in
print("CoreData", "Initiated \(persisterStoreDescription)")
guard error == nil else {
print("CoreData", "Unresolved error \(error!)")
return
}
}
self.persisterContainer = container
self.viewContext = container.viewContext
self.viewContext.automaticallyMergesChangesFromParent = true
self.privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
self.privateContext.persistentStoreCoordinator = container.persistentStoreCoordinator
self.privateContext.automaticallyMergesChangesFromParent = true
}
func createTemporaryViewContext() -> NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = self.privateContext
context.automaticallyMergesChangesFromParent = true
return context
}
func saveTempViewContext(tempContext context: NSManagedObjectContext, completion: ((CoreDataStack.SaveStatus) -> Void)? = nil) {
guard context.hasChanges || privateContext.hasChanges else {
completion?(.hasNoChanges)
return
}
context.performAndWait {
do {
try context.save()
}
catch {
completion?(.error)
return
}
}
privateContext.perform { [weak self] in
do {
try self?.privateContext.save()
completion?(.saved)
}
catch {
self?.privateContext.rollback()
completion?(.rolledBack)
}
}
}
class ViewController: UIViewController {
#objc
func save(_ sender: UIBarButtonItem) {
let coreDataStack = CoreDataStack()
let tempMainContext = coreDataStack.createTemporaryViewContext() //child main context from private context
var people = People(context: tempMainContext)
self.people.name = "John Doe"
self.people.age = 25
coreDataStack.saveTempViewContext(tempContext: tempMainContext) { status in
print(status)
}
}
}
I have "privateContext" attached to coordinator
I've created "tempMainContext" from private context
When I call "saveTempViewContext" I want to save tempMainContext which pushes changes to parent (privateContext) and this privateContext saves to persistent store
So the error occurred in line
privateContext.hasChanges
I know thats this line executes in main thread. And I need to call method "perform" or "performAndWait" to perform on the right queue.
like this
var contextHasChanges: Bool = false
var privateContextHasChanges: Bool = false
context.performAndWait {
contextHasChanges = context.hasChanges
}
privateContext.performAndWait {
privateContextHasChanges = privateContext.hasChanges
}
guard context.hasChanges || privateContext.hasChanges else {
completion?(.hasNoChanges)
return
}
But it so weird to call "performAndWait" just to check that context has changes. And when I call "performAndWait" it block current thread in my case it MainThread. And I don't want block the main thread even for short time.
How could we resolve this issue ?
UPD 1
attached debug stack
UPD 2
In my CoreDataStack init method in below I'v added code.
I just check if the private context has changes and It will crash
let privateContextHasChanges = privateContext.hasChanges
I think it's because at that line we are in MainThread and we touch private context which init with "privateQueueConcurrencyType" and I investigate that if I touch other property for example "privateContext.name" or "privateContext.parent" it works fine.
But if I touch property like this:
privateContext.hasChanges
privateContext.registeredObjects
privateContext.updatedObjects
maybe other
it will crash again
So I can make a conclusion that these properties are not thread safe.
Can anyone confirm that ?
UPD 3
After I'v read post Unexpected Core Data Multithreading Violation
I'v made conclusions:
If I'm on the main thread and my context's type is .main I do not need any changes and I safe
If I'm on some place and I don't know want kind of thread I'm on I always need to do "perform" or "performAndWait" to synchonize queue attached to context.
Almost always do "perform" and not "performAndWait" except you can't do without it.
context.hasChanges isn'n thread safe. It requires to be used in the context thread.
https://developer.apple.com/forums/thread/133378
I am facing this issue once or twice a day for the last week when I open my app & the app tries any save operation on the context, I still can't find a way to reproduce it.
I have searched many question on SO for fix, but most of them point 2 issues
Core Data Migration issue(which I don't have as I am on the same Model version no.)
failure of loading the persistent store (which is also doesn't happen in my case as my Core Data Stack doesn't initialise the main UI if the loadPersistentStores method on the persistentContainer fails)
I am using the Core Data stack setup mentioned in the below link:
https://williamboles.me/progressive-core-data-migration/
Here is my CoreData Setup class:
lazy var persistentContainer: NSPersistentContainer = {
let persistentContainer = NSPersistentContainer(name: "ABC")
let description = persistentContainer.persistentStoreDescriptions.first
description?.shouldInferMappingModelAutomatically = false //inferred mapping will be handled else where
description?.shouldMigrateStoreAutomatically = false
description?.type = storeType
return persistentContainer
}()
lazy var managedObjectContext: NSManagedObjectContext = {
let context = self.persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.automaticallyMergesChangesFromParent = true
return context
}()
lazy var _managedObjectContext: NSManagedObjectContext = {
let context = self.persistentContainer.viewContext
context.automaticallyMergesChangesFromParent = true
return context
}()
// MARK: - Singleton
private static var privateShared : CoreDataManager?
class func shared() -> CoreDataManager { // change class to final to prevent override
guard let uwShared = privateShared else {
privateShared = CoreDataManager()
return privateShared!
}
return uwShared
}
class func destroy() {
privateShared = nil
}
// MARK: - Init
init(storeType: String = NSSQLiteStoreType, migrator: CoreDataMigratorProtocol = CoreDataMigrator()) {
self.storeType = storeType
self.migrator = migrator
}
// MARK: - SetUp
func setup(completion: #escaping () -> Void) {
loadPersistentStore {
completion()
}
}
// MARK: - Loading
private func loadPersistentStore(completion: #escaping () -> Void) {
migrateStoreIfNeeded {
self.persistentContainer.loadPersistentStores { description, error in
guard error == nil else {
fatalError("was unable to load store \(error!)")
}
completion()
}
}
}
private func migrateStoreIfNeeded(completion: #escaping () -> Void) {
guard let storeURL = persistentContainer.persistentStoreDescriptions.first?.url else {
fatalError("persistentContainer was not set up properly")
}
if migrator.requiresMigration(at: storeURL, toVersion: CoreDataMigrationVersion.current) {
DispatchQueue.global(qos: .userInitiated).async {
self.migrator.migrateStore(at: storeURL, toVersion: CoreDataMigrationVersion.current)
DispatchQueue.main.async {
completion()
}
}
} else {
completion()
}
}
And I initialise the Core Data stack using the following code in the App Delegate:
CoreDataManager.shared().setup {[unowned self] in
self.showMainUI()
}
My App crashes after the Home Controller is loaded & some part of my code does a save operation on certain NSManagedObject Model
This how I save to Context:
let context = CoreDataManager.shared().managedObjectContext // background context
context.performAndWait {
if let entityDescription = NSEntityDescription.entity(forEntityName: Entity_Name, in: context) {
if let runEntityObject = NSManagedObject(entity: entityDescription, insertInto: context) as? MY_Model {
// Create the Object
guard context.hasChanges else { return }
do {
try context.save() // Crashes here once or twice a day :(
}
catch {
print(error.localizedDescription)
}
}
}
}
Some SO answers also mention of threading issues but I am using the performAndWait Block so the save happen on the same queue
Would be really helpful If someone pointed me in the right direction regarding this issue
After going through my AppDelegate file many times, I found that I was doing a Core Data save operation in the applicationDidBecomeActive method which is also called when the app starts from a suspended state.
So if my Core Data stack setup closure didn't finish before the applicationDidBecomeActive is called the app would crash.
After removing it, the app was working fine without any crashes
I'm working with old xcdatamodel, it was created in xcode 7.3 (that's a crucial since I don't have the following issue on modern models). At the same time, this issue is not cured by simple changing Tool Version to Xcode 9.0 for my xcdatamodel.
I'm fetching data in for loop, in the thread of context I use for fetching data. When I try to fetch the entity that has already been fetched once, coreData crashes with EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP). Zombie tracking says [CFString copy]: message sent to deallocated instance 0x608000676b40.
This is the concept of what I do:
LegacyDatabaser.context.perform {
do {
for _ in 0..<10 {
let entity = try self.legacyDatabase.getEntity(forId:1)
print(entity.some_string_property) // <- crash here
}
} catch {
// ...
}
}
Here is the context initializer:
class LegacyDatabaser {
static var context: NSManagedObjectContext = LegacyDatabaseUtility.context
// ...
}
And
class LegacyDatabaseUtility {
fileprivate class var context: NSManagedObjectContext {
//let context = NSManagedObjectContext(concurrencyType:.privateQueueConcurrencyType)
//context.persistentStoreCoordinator = storeContainer.persistentStoreCoordinator
//return context // This didn't help also
return storeContainer.newBackgroundContext()
}
private static var storeContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name:"MyDBName")
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()
}
Here is the data fetcher:
func getEntity(forId id: NSNumber) throws -> MyEntity? {
// Create predicate
let predicate = NSPredicate(format:"id_local == %#", id)
// Find items in db
let results = try LegacyDatabaseUtility.find(predicate:predicate, sortDescriptors:nil, in:LegacyDatabaser.context)
// Check it
if results.count == 1 {
if let result = results.first as? MyEntity {
return result
} else {
return nil
}
} else {
return nil
}
}
And:
static func find(predicate:NSPredicate?, sortDescriptors:[NSSortDescriptor]?, in context: NSManagedObjectContext) throws -> [NSManagedObject] {
// Create a request
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName:"MyEntity")
// Apply predicate
if let predicate = predicate {
fetchRequest.predicate = predicate
}
// Apply sorting
if let sortDescriptors = sortDescriptors {
fetchRequest.sortDescriptors = sortDescriptors
}
// Run the fetchRequest
return try context.fetch(fetchRequest)
}
I don't address the context somewhere in a parallel, I'm sure I use the correct thread and context (I tested the main context also, the same result). What I'm doing wrong, why re-fetching the same entity fails?
If anyone catches any quirk crashes like one I described above, check the name of properties in your NSManagedObject. In my case, crashes were on call new_id property which is, I guess, kinda reserved name. Once I renamed this property, the crashes stopped.