Create a fake NSManagedObjectContext for preview purposes? - swift

I want a "fake" preview NSManagedObject to display as an example. However, I don't want to have it cluttering up my regular context. Here's what I'm assuming could work:
Create a new NSManagedObjectContext and store the preview NSManagedObject there
let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let person = Person(moc, "Person Name")
What happens to the new context in this case? Is it slowly filling up storage or will it get removed if no save() is called and the View removed?
Remove the newly created NSManagedObject when leaving the view
SomeView()
.onDisappear() {
moc.delete(person)
PersistenceController.shared.save()
}
This might not work if the .onDisappear() function doesn't get called, e.g. when the user leaves the app while that particular view is still open.
Add the fake preview NSMangagedObject to the regular context and filter it out everywhere else (probably the worst idea)
Create a new global context only for fake NSManagedObjects
Not sure which of these options is best or if there's a better way I haven't thought of. Thanks :)

I created a new PersistenceController in memory as Joakim Danielson suggested in the comments, but using it alongside my other context was causing crashes and error messages. Simply creating a new NSManagedObjectContext, however, worked fine.
It is not saved to my "real" context, either (Apple Documentation):
Changes to managed
objects are held in memory, in the associated context, until that
context is saved to one or more persistent stores
let tempMoc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
Usage
let person = Person(tempMoc, "Person Name")

Related

SwiftUI + macOS + document based application problems

I am trying to build a document based SwiftUI application with core data enabled. Starting from the template in Xcode (11.5): New project -> macOS + App -> Swift + SwiftUI + "Create document-based application" + "Use Core data". After that I try to add an entity in model editor with just two attributes, "id" and "amount". The model is called "Trans" and codegen is set on "Class Definition".
In the provided Content view I add the code below so I can access the managed object context.
#Environment(\.managedObjectContext) var moc
So far it's working as expected. After that I try to add a simple fetch request.
#FetchRequest(entity: Trans.entity(), sortDescriptors: []) var trans: FetchedResults<Trans>
Then this error comes up in the console.
No NSEntityDescriptions in any model claim the NSManagedObject subclass 'myapp.Trans' so +entity is confused. Have you loaded your NSManagedObjectModel yet?
I am quite new to Core Data so I don't even know where to start looking. Is this broken? Or am I supposed to add more code to get it to work? Can someone please point in the right direction?
I had this exact problem creating an app using Xcode12 (Beta 2) and the multi-platform option.
What works is to divide the code up so that your context is fully initialized before you try to use it.
So take the following line of code and split it into two lines of code:
// Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
// Add `#Environment(\.managedObjectContext)` in the views that will need the context.
let contentView = ContentView().environment(\.managedObjectContext, persistentContainer.viewContext)
Dividing it up into two lines:
// Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
// Add `#Environment(\.managedObjectContext)` in the views that will need the context.
let context = persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)
It's a small change, but everything compiles and runs correctly now.

How to read an existing core data persistence store, without creating a new one?

I am trying to migrate a Core Data database to Realm. I'm okay with changing the APIs in my code (I actually did a whole rewrite of the app), but the problem is migrating the existing data stored on users' devices.
To read data from Core Data, I first create a NSManagedObjectContext, and set its persistentStoreCoordinator to a coordinator:
// app delegate, didFinishLaunching
var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
To create this coordinator, I do:
let modelURL = Bundle.main.url(forResource: "MyModel", withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)!
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
Then at this point, I don't know what to do next. Normally in a Core Data app, I would add a persistent store at a desired URL...
let url = aDesiredURL.appendingPathComponent("MyPersistentStore.sqlite")
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: [NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true])
...just like the Core Data boilerplate that Xcode generates for you when you check "Use Core Data" when creating a project.
However, since I'm migrating, I don't want to add a new persistent store. If there is already a "MyPersistentStore.sqlite" file stored at aDesiredURL, I just want to read from that.
From my experimentation, addPersistentStore seems to create a new MyPersistentStore.sqlite file if it doesn't exist, and read from it if it already exists, but I want a method to read from MyPersistentStore.sqlite if it exists, and not create a new file (maybe throw an error?) if it doesn't.
I looked through the methods of NSPersistentStoreCoordinator in its documentation, and found persistentStore(for:) which sounded promising. However, it returns nil whether or not the .sqlite file exist.
I also tried not doing anything after creating the coordinator, but that didn't work either: I wasn't able to retrieve any managed objects from the context (the fetch request returned 0 objects).
Code that fetches the entities for reference:
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "MyEntity")
let managedObjects = try managedObjectContext.fetch(fetchRequest)
print(managedObjects.count)
How can I only read from the .sqlite file representing the Core Data persistent store, without creating a new one?

CoreData, threading, main context and private context - works 99% of the time but

In my app I use two contexts: app delegate main context and a private context.
The private context is set as follows:
var privateContext: NSManagedObjectContext = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)
privateContext.persistentStoreCoordinator = context.persistentStoreCoordinator
I also set an observer on the private context to trigger a save via main context:
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(MyView.managedObjectContextDidSave(_:)), name: NSManagedObjectContextDidSaveNotification, object: self.privateContext)
I trigger a data download with a callback to the current ViewController. During the download, I process all objects in:
privateContext.performBlock {
// process objects
....
// now save
if self.privateContext.hasChanges {
privateDataManager.save()
}
}
The save in the private context, triggers the observer and this code gets invoked:
dispatch_async(AppUtils.GlobalMainQueue, {
self.context.mergeChangesFromContextDidSaveNotification(notification)
})
The problem is that every now and then, not all changes get persisted. Cannot say when or why - when I debug it it always works...
How do I know I have a problem? Well, I compare object count before and after the download. To add more 'colour':
Every download of data adds some new records. The app then selects which records are out-of-date and marks them for deletion (sets a flag on a record). It then saves them (new and 'to be deleted') in the private context. The 'save' triggers a 'merge into primary context' notification.
In theory, this notification triggers the 'merge' in a synchronous manner.
After the merge, assuming it does happen in-order, there is data reload - this reload only loads records that do not have the 'deleted' flag set.
The problem I am having is that the 'merge' does not seem to always happen before I reload (that's the only way I can explain this).
My question, assuming my analysis is correct, is how to force the merge to happen before the reload? Does the notification not happen in a synchronous manner?
I guess I could always save the private context and instead of triggering the notification, simply create a notification object using private context and force trigger the merge. But I would like to understand why the above code does not work as expected.
The main source on which I based my code can be found here.

Swift bad access error on empty value even with check

I am in the process of transitioning over to Swift from Obj-C and I think I have a fairly good understanding of the ? ! concepts. I am however experiencing a bad access crash on a field that is nil.
I am using
var cell :ContactIInfoTableViewCell!
cell = tableView.dequeueReusableCellWithIdentifier("notes", forIndexPath: indexPath) as! ContactIInfoTableViewCell
if let clinic = receivedVisitDetails?.clinicNotes {
cell.textLabel?.text = clinic
}
Which produces the error if empty. My understanding was that the IF statement would stop this issue but it hasn't.
What am I doing wrong here?
UPDATE
This is a detail view for the core data fetch.
NSManagedObject is set as
var receivedVisitDetails: VisitDetails! = nil
The prepareForSeague on the previous view
let visits:VisitDetails = fetchedResultsController.objectAtIndexPath(indexPath!) as! VisitDetails
taskController.receivedVisitDetails = visits
The crash only happens when the clinicNotes is null.
After all edits and comments, the problem is in:
#NSManaged var clinicNotes: String
It's optional in your Core Data model, but not in your class. Should be changed to:
#NSManaged var clinicNotes: String?
Here's similar problem where Imanou suggests mogenerator, which can handle it for you. It automatically generates two classes for each managed object. Human part is suitable for editing (not touched by mogenerator when updating your model) and machine is not suitable for editing (rewritten every time you do update your model). Human part inherits from machine part.
Whenever I'm working with Core Data I do use it. I strongly suggest it as well. You can also add it to build phase, so, it automatically update your machine classes when you're building your project.
When using this construction, you don't have to use ? as it is unwrapped for you.
try
if let clinic = receivedVisitDetails.clinicNotes {
cell.textLabel?.text = clinic
}

executeFetchRequest:error: A fetch request must have an entity

I had a project that was working fine. It had "Core Data" checked, and had the data model all set up. I only started adding a couple entities and properties tonight. When I try to run my app now, it won't even load. It just crashes on me.
Here's the error:
'NSInvalidArgumentException', reason: 'executeFetchRequest:error: A fetch request must have an entity.'
I'm really scared because I don't have a backup of this and if I can't get it working I don't know what I'll do. :)
Thanks in advance!
EDIT:
I got fed up with my data, so I just copied a new blank xcdatamodel to my project and I'm going to start fresh. Thanks for the help!
My issue is I didn't use the same name for Entity and Class. Trivial solution to fix it is by giving them the same name.
If you are using MagicalRecored with Swift:
Make sure you use #objc directive in the Swift NSManagedObject subclass to make the class accessible to Objective-C code from the MagicalRecord library
#objc(MyEntity)
class MyEntity: NSManagedObject {
#NSManaged var myAttribute: Int16
}
After searching all over for a solution, what fixed it for me was doing a Clean/Build in Xcode.
Product->Clean, Product->Build, then try running it.
It seemed as if my data got corrupted, so I deleted my data model and the database in the iPhone simulator, and started fresh.
I had the same error.
For me, it is because I have added a new Model Version, but I did not set it as "Current Version". Careless me! To fix, select the xcdatamodel, click Design > Data Model > Set Current Version. The xcdatamodel file will then have a green tick.
Hopes that helps.
Also, make sure that your .xcdatamodeld file is in the "Copy Bundle Resources" phase of your Build Phases.
Here's what fixed it for me:
As I was converting to Swift 3, Xcode was giving me an error when declaring a new NSFetchRequest, saying that it needed a type. After adding the type, I did what anyone else would have assumed; if the request is typed, why specify an entity name? So, I removed it.
It actually was my mistake.
Swift 2.2:
let request = NSFetchRequest(entityName: "MyEntity")
When I first converted to Swift 3:
let request = NSFetchRequest<MyEntity>()
That was giving me an error. I ended up with this:
let request = NSFetchRequest<MyEntity>(entityName: "MyEntity")
And everything works fine. Personally, I'm not sure why it needs to have an entity name specified, if you're typing the request. Maybe they'll update that at some point (I hope)
i found this solution in the apple develper forum and it was exactly my problem!
the solutions is that the context must be defined inside the struct App.
not in the environment parameter
import SwiftUI
#main
struct CoreDataDemoApp: App {
private let context = CoreDataStack.context.
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, context)
}
}
}
Check if,
the entity is present in the xcdatamodel file.
entity name used are same.
If you are using Swift 3 and Core Data's new stack syntax:
var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MyAppModel")
container.loadPersistentStores(completionHandler: {
(storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
} else {
print(storeDescription)
}
})
return container
}()
Then you should be using this fetch syntax:
let request: NSFetchRequest<Client> = Client.fetchRequest()
I had this error on the first fetch after app launches when using different variations:
let request: NSFetchRequest<NSFetchRequestResult> = Client.fetchRequest()
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Client")
I did stumble across the same precise error upon taking my first steps into Core Data (and iOS 11 and Swift 4). I started off a book (sixth edition meant to target Swift 4 but presumably including some legacy stuff).
As suggested in the book my code was:
let fetchRequest = NSFetchRequest<ReminderData>()
let entity = ReminderData.entity()
fetchRequest.entity = entity
do {
let rows = try managedObjectContext.fetch(fetchRequest)
} catch {
fatalError("Unresolved error")
}
It turned out that all I got from ReminderData.entity() is nil. Not sure if I did something wrong when setting up the data model or ... Apple's docs say that NSManagedObject.entity() must not be overwritten?
Long story short, the Codegen file ReminderData+CoreDataProperties.swift did include the solution:
#nonobjc public class func fetchRequest() -> NSFetchRequest<ReminderData> {
return NSFetchRequest<ReminderData>(entityName: "ReminderDB")
}
which was all I had to use to end-up with a proper NSFetchRequest, no fiddling with the NSEntityDescription, problem gone!
let fetchRequest = NSFetchRequest<ReminderData>(entityName: "ReminderDB")
do {
let rows = try managedObjectContext.fetch(fetchRequest)
} catch {
fatalError("Unresolved error")
}
I built clean, and that didn't fix it. Then I deleted the app, and that didn't fix it. Then I built clean and deleted the app AT THE SAME TIME, and that fixed it.
Just add the same problem. I copied all my entities. Deleted the data model, recreated an empty one and pasted the entities back into the new data model. Solved my issue.
First I downloaded the app's data through the Organizer (to see what was happening) and noticed that it offered me to save it under a previous project name. This puzzled me. So I exited XCode 4.6.1, deleted the app (and its data) from my iPhone, and came back.
This time I got an error saying Cannot create an NSPersistentStoreCoordinator with a nil model. So I looked into the AppDelegate.m file and changed the URLForResource in the - (NSPersistentStoreCoordinator *) persistentStoreCoodinator method. It was set to the name of my app, and I changed it to 'Model' so as to match the name of my Model.xcdatamodeld.
It's working now.
This happened to me when I was fetching from the wrong database. My application has 3 sqlite databases, and of course 3 ManagedObjectContext instances. Well I was submitting the wrong ManagedObjectContext to a method asking it to query a table that didn't exist in the ManagedObjectContext I submitted. After using the correct ManagedObjectContext, all was good.
I think the original question/problem, and also the issue that most of these answers fixes (just in different ways) is just a real simple one:
Anytime you modify your core data (like adding an entity as you mention), you either have to delete all existing data (if you haven't published your app yet), or add a new version to your model.
Just thought I would post this answer, even though this is an older question, because this answer seems pretty obvious and so far hasn't been discussed in any of the questions or comments I read above.
You can also use setter method from CoraData ... Just do something like this...
On your CustomCoreDataManager.m
import "ObjectiveRecord.h"
call init method like this
(instancetype)init {
self = [super init];
if (self) {
[[CoreDataManager sharedManager] setModelName:#"YourModelName"];
}
return self; }
Hope this helps to someone...
Maybe you are trying to load a Database from a different/the wrong bundle?
For instance from or within a Framework?
I had this issue and solved it by loading the DB from the bundle of the related Framework. And then it all worked fine!!
Swift 4 + MagicalRecord:
let frameworkBundle = Bundle(for: AClassFromTheFramework.self)
let managedObjectModel = NSManagedObjectModel.mergedModel(from: [frameworkBundle])
MagicalRecord.setShouldAutoCreateManagedObjectModel(false)
NSManagedObjectModel.mr_setDefaultManagedObjectModel(managedObjectModel)
MagicalRecord.setupCoreDataStack(withAutoMigratingSqliteStoreNamed: "db.sqlite")
And voila !
I faced same issue, actually i was calling MyEnty instead of MyEntity so please re-check what names you have given to your entities and call the same and also check whether you are calling same attributes that you have defined like name
In my case, it was because this dropdown was not set to "Current Product Module" in the Data Model Inspector in Xcode (13.4.1):
Once I set that, it stopped crashing.
Hope this helps!