How to use Core Data's ManagedObjectModel inside a framework? - ios

I'm trying to migrate a specific part of one of my apps into a framework so that I can use it in my app itself and in one of those fancy new iOS 8 widgets. This part is the one that handles all my data in Core Data. It's pretty straight forward to move everything over and to access it. I'm just having trouble accessing my momd file in there.
When creating the NSManagedObjectModel I still try to load the momd as illustrated in Apple's code templates:
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"MyApp" withExtension:#"momd"];
__managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
Unfortunately, modelURL stays nil and thus MyApp crashes when accessing the Core Data stack with this error:
2014-08-01 22:39:56.885 MyApp[81375:7417914] Cannot create an NSPersistentStoreCoordinator with a nil model
2014-08-01 22:39:56.903 MyApp[81375:7417914] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Cannot create an NSPersistentStoreCoordinator with a nil model'
So, what's the right way to do this when working inside a framework with Core Data?

I'm a bit late for flohei's issue, but hopefully this helps anybody else who wanders by. It is possible to get this to work without having to perform script-fu to copy resources around!
By default Apple's Core Data template gives you something like this:
lazy var managedObjectModel: NSManagedObjectModel = {
let modelURL = NSBundle.mainBundle().URLForResource("MyCoreDataModel", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()
That would load your Core Data resources out of the main bundle. The key thing here is: if the Core Data model is loaded in a framework, the .momd file is in that framework's bundle. So instead we just do this:
lazy var managedObjectModel: NSManagedObjectModel = {
let frameworkBundleIdentifier = "com.myorg.myframework"
let customKitBundle = NSBundle(identifier: frameworkBundleIdentifier)!
let modelURL = customKitBundle.URLForResource("MyCoreDataModel", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()
That should get you up and running.
Credit: https://www.andrewcbancroft.com/2015/08/25/sharing-a-core-data-model-with-a-swift-framework/

You need to drag the xcdatamodeld file and drop it in the Build Phases | Compile Sources for the targets that use the framework. Then when the target runs its [NSBundle mainBundle] will contain the model (momd file).

#Ric Santos was almost there. I think you just need to make it an "mom" extension rather than "momd", then it will run.
lazy var managedObjectModel: NSManagedObjectModel = {
let modelURL = NSBundle(forClass: self.dynamicType.self).URLForResource(self.dataModelName, withExtension: "mom")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()

I may be a little late with answering this, but here's what solved my issue:
With my framework I'm delivering also a bundle with resources like images and other stuff. Putting xcdatamodeld file there didn't give anything as this file being built with the project and as a result you get a momd folder in your app bundle (which actually is missing in our case)..
I have created another target, not framework, but app, built it and copied the momd from its app bundle to my separate bundle in the project (the one that goes with framework).
After doing this you just need to change your resource url from main bundle to the new one:
// ...
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:#"separate_bundle" ofType:#"bundle"];
NSURL *modelURL = [[NSBundle bundleWithPath:bundlePath] URLForResource:#"your_model" withExtension:#"momd"];
// ...
Worked fine for me. The only thing I'm aware of is App Store Review which I didn't get to yet.
So if you've found a better solution, please share.
EDIT
Found better solution. You can build the model yourself. From Core Data Programming Guide:
A data model is a deployment resource. In addition to details of the
entities and properties in the model, a model you create in Xcode
contains information about the diagram—its layout, colors of elements,
and so on. This latter information is not needed at runtime. The model
file is compiled using the model compiler, momc, to remove the
extraneous information and make runtime loading of the resource as
efficient as possible. An xcdatamodeld “source” directory is compiled
into a momd deployment directory, and an xcdatamodel “source” file is
compiled into a mom deployment file.
momc is located in /Developer/usr/bin/. If you want to use it in your
own build scripts, its usage is momc source destination, where source
is the path of the Core Data model to compile and destination is the
path of the output.
By "/Developer/usr/bin/" they mean "/Applications/Xcode.app/Developer/usr/bin/"
You can add a script in you target scheme and compile this automatically before each build (or after, don't think it matters). This is in case if you change the model during development.
Something like this:
mkdir -p "${BUILT_PRODUCTS_DIR}/your_model_name.momd"
momc "${SRCROOT}/your_model_path/your_model_name.xcdatamodeld" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework/your_bundle_name.bundle/your_model_name.momd"

The model resource is no longer accessible via the mainBundle, you need to use bundleForClass: like so:
NSURL *modelURL = [[NSBundle bundleForClass:[self class]] URLForResource:#"MyApp" withExtension:#"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

I assume you only require the data model.
If so, I find the following is consistently the most successful method for copying across a data model file from one project to another...
Delete any current copies of .xcdatamodeld file that reside in the target project.
Close Xcode.
Using Finder (or cmd line);
Select your Xcode project folder that contains the original .xcdatamodeld file.
Make a copy of the original .xcdatamodeld file.
Move the copy of the original .xcdatamodeld file to your target project.
Then...
Open Xcode.
Using the Menu command "Add Files to >your project<", find and add the copy of the original .xcdatamodeld file to your project.
Rename the original .xcdatamodeld file (if necessary) using Project Navigator.
"Build & Run" your target project.
Does this help?

If you try to have core data in your framework do like this.
in YourFramework
- add new file / core data / Data model ...
- create all object ....
When creating new project that will use YourFramework be sure that core data is on
This will create all boiler plate inside AppDelegate.
in Test project
- add Framework
- add Framework as embedded framework
- DELETE .xcdatamodeld file
- in AppDelegate :
change - (NSManagedObjectModel *)managedObjectModel method into :
NSBundle * testBundle = [NSBundle bundleWithIdentifier:#"YourFramework bundle id "];
NSURL *modelURL = [testBundle URLForResource:#"Model" withExtension:#"momd"];
where YourFramework bundle id is Bundle Identifier of YourFramework (in General / Bundle Identifier)
and Model is name of your .xcdatamodeld file in YourFramework
This works.
Hope it helps.

Related

Shipping core data with iOS app

I am developing an dictionary app that has around 10,000 words in it. I am using core data to store all the data and the language that I am using is swift.
All I want is to ship this data with the app so that when the app is downloaded from the app store, it contains all the data.
I have already searched on this and found that it can be done by including SQLite file to the project. But really do not know how to do that and where is the simulator directory in El Capitan.
I am just a beginer in iOS, so kindly someone explain it to me in very simple steps.
This could be quite a lengthy answer. It would be best to explain it as a tutorial. The following link will show you what to do.
http://www.appcoda.com/core-data-preload-sqlite-database/
I found the start of the article/tutorial and the end useful for what you are trying to do.
I recently preloaded data into an app for a Quiz, using a separate Xcode simulator to create the 3 SQLite files needed. This article will show you how to find them, how to "bundle" them into your Xcode project that you want to 'Pre-Load' and will tell you the code to add inside your 'AppDelegate.swift' file and where.
There are 3 things that will help you avoid problems in completing your project...
Ensure the Entity and Attribute names (in your Data Model (Core Data)) for the data you are creating in the Simulator (separate project) are identical to the ones in the project you want to 'PreLoad'. [ Otherwise the Preloading WONT work ]
The code to add in 'AppDelegate.swift' is designed to direct your App as to where to go for the preLoaded Data (see the attached Tutorial), however, I found I needed to tweek the code to make it work...(noted below)
Nothing is deleted from the 'AppDelegate.swift' file, merely added to and the names of the 3 SQLite files added to it... e.g.
In 'AppDelegate.swift' in the Core Data stack... you will find...
lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle.mainBundle().URLForResource("EoMQ", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()
Leave this alone, but direcly underneath this you need to have the following code...
// ---------------------------------------------------
// Create the coordinator and store
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("Q100cdCoreData.sqlite") // originally = "SingleViewCoreData.sqlite" - then changed to new name... from imported sqlite files
// ---------------------------------------------------
// ** Load the Already made DB from Simulator **
if !NSFileManager.defaultManager().fileExistsAtPath(url.path!) {
let sourceSqliteURLs = [NSBundle.mainBundle().URLForResource("Q100cdCoreData", withExtension: "sqlite")!, NSBundle.mainBundle().URLForResource("Q100cdCoreData", withExtension: "sqlite-wal")!, NSBundle.mainBundle().URLForResource("Q100cdCoreData", withExtension: "sqlite-shm")!]
let destSqliteURLs = [self.applicationDocumentsDirectory.URLByAppendingPathComponent("Q100cdCoreData.sqlite"), self.applicationDocumentsDirectory.URLByAppendingPathComponent("Q100cdCoreData.sqlite-wal"), self.applicationDocumentsDirectory.URLByAppendingPathComponent("Q100cdCoreData.sqlite-shm")]
for index in 0 ..< sourceSqliteURLs.count {
do {
try NSFileManager.defaultManager().copyItemAtURL(sourceSqliteURLs[index], toURL: destSqliteURLs[index])
} catch {
print(error)
}
}
}
The 3 files I had created in the simulator were called "Q100cdCoreData" but with three different extensions... (a) .sqlite (b) .wal (c) .shm
But you need to go through the tutorial and understand the process.
Add the sqlite file to project.
You will call
[[NSFileManager defaultManager] copyItemAtURL:preloadURL toURL:storeURL error:&error]
Before:
[_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error]
Where preloadURL will be the URL for file in application bundle. The storeURL is the path to your sqlite file for core data (normally in application directory).
So the file will be copied from bundle to app directory if it doesn't exist and fail otherwise.

Coredata: Multiple xcdatamodeld files in the same iOS project?

Is it possible to serve two xcdatamodeld core data in the same project and load each according to a conditional?
I have BTPModel.xcdatamodeld and FTModel.xcdatamodeld
According to the comments this line below does this:
NSManagedObjectModel model = [NSManagedObjectModel mergedModelFromBundles:nil];
// looks up all models in the specified bundles and merges them; if
nil is specified as argument, uses the main bundle
Can I do something like this? (pseudo code)
if (config == #"FT") {
model = [NSManagedObjectModel load:#"FTModel.xcdatamodeld"];
} else {
model = [NSManagedObjectModel load:#"BPTModel.xcdatamodeld"];
}
UPDATE:
I have now tried this
NSURL *url = [[NSBundle mainBundle] URLForResource:#"F11iModel" withExtension:#"xcdatamodeld"];
Without any luck. url remains null.
UPDATE
Extension is momd. Now it works!
NSURL *url = [[NSBundle mainBundle] URLForResource:#"F11iModel" withExtension:#"momd"];
Of course you can. Just a few things to pay attention to:
The loading of a specific model is not done with some load method, but with initWithContentsOfURL. You get the URL with the main bundle's URLForResource. You can pass different resource names to this according to the configuration information.
I would recommend that you also use different persistent stores to make sure there is no attempt to open a the store with the wrong model (which will crash your app).

NSManagedObjectModel is nil

enter code hereI am working on an import tool, following the pattern objc.io did at https://github.com/objcio/issue-4-importing-and-fetching .
At an early point in the program, where I am creating an instance of NSManagedObjectModel from a url, it is returning nil. When I run the program, the error I get later is "Cannot create an NSPersistentStoreCoordinator with a nil model".
Attached is a screenshot at a breakpoint after I create the model. I've looked around here, and have verified that:
HistologyDataImporter.xcdatamodeld belongs to HistologyDataImporter (target membership)
The NSString I'm using the build the path matches the name of the model file ("HistologyDataImporter")
I have also tried passing "mom" as the extension to URLByAppendingPathComponent, but the model is still nil when run.
Why is [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL] still returning nil?
Because you're using the wrong location. What you have is:
NSString *path = #"HistologyDataImporter";
NSURL *modelURL = [NSURL fileURLWithPath:path isDirectory:NO];
moelURL = [modelURL URLByAppendingPathComponent:#"momd"];
If you call fileURLWithPath: with a relative path like this, it's assumed to be relative to the current working directory. Your code does not set the working directory, but the value for modelURL suggests it's somewhere in Xcode's derived data folder. That's not surprising when running the app from Xcode, but the model file is not in that directory.
You need to figure out where HistologyDataImporter.momd is located and make sure that modelURL actually points to that location. If you put the model file on your desktop, you could use the same desktopURL that you have in main() and add the model filename to that.

Load data into my app xcdatamodel

My app must start for end users with data already in the database so that info is displayed to them when they use the app.
My problem: how can I load the data into the app database?
There's an "import..." option on xcode (on Editor when selecting xcdatamodeld) but I cannot understand what is the file type required - I've tried .xls, .csv and .sqlite and none is "importable".
Help!
Add the database to your bundle when you ship your app. On initial startup, you look to see if the database lives in the local file system. If not (and it won't be the first time you start up), you copy the database from the bundle to the local file system, open it, and use it from there. Don't try to use it from the bundle, that's set to read only.
I'm assuming you're using a SQLite database or some other type of file. You can add a file to your bundle using these instructions: How do I add files to the resources folder in XCode?
If the data is already in the .sqlite format (see note below), copy that file to the project (move the file to the Xcode project directory, and then from Xcode choose File -> Add files to "Project name", and find the file in your computer directory.
In the main AppDelegate.m file under the persistentStoreCoordinator function (see this tutorial for setting up sqlite/Core Data in your project), write the code to copy from the main bundle resources to the app resources directory, if the file doesn't exist:
NSURL *dbUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory]
stringByAppendingPathComponent: #"yourDBName.sqlite"]];
if (![[NSFileManager defaultManager] fileExistsAtPath:[dbUrl path]]) {
NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"yourDBName" ofType:#"sqlite"]];
NSError* err = nil;
if (![[NSFileManager defaultManager] copyItemAtURL:bundleURL toURL:dbUrl error:&err]) {
NSLog(#"Could not copy main bundle data");
}
else { NSLog(#"Main bundle data successfully copied"); }
}
It is important that your preloaded .sqlite database from the main bundle have been created properly from within an Xcode project (iOS apps only recognize specially-formatted .sqlite files).
Once this is loaded, it will be recognized as the app's database as long as the Core Data managed object context has its persistent store coordinator set to this .sqlite file, and that its structure matches that of the core data model.

Core data model file is not in bundle

I rearranged the files in my program folder, grouping them into appropriate subfolders. I made sure they all show up in the compiled sources list, including the 'xcdatamodeld' file.
However, creating a managed object model is not working with the following code:
if (mom_ != nil) {
return mom_;
}
self.mom = [NSManagedObjectModel mergedModelFromBundles:nil];
return mom_;
I examined [NSBundle mainBundle], and specifically
[[NSBundle mainBundle] pathForResource:#"Words" ofType:#"xcdatamodeld"]
and
[[NSBundle mainBundle] pathForResource:nil ofType:#"xcdatamodeld"]
They both return nil. I can see that other resources are there when I check for them by name and type.
There is a folder called "Words.momd" in the app bundle file.
What might have happened and how can it be fixed?
Try using:
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"Words" withExtension:#"momd"];
You want to load the compiled data model, vs. the xcdatamodeld file:
A data model is a deployment resource. In addition to details of the entities and properties in the model, a model you create in Xcode contains information about the diagram—its layout, colors of elements, and so on. This latter information is not needed at runtime. The model file is compiled using the model compiler, momc, to remove the extraneous information and make runtime loading of the resource as efficient as possible.
(source)
You can use MagicalRecords framework for it:
https://github.com/magicalpanda/MagicalRecord
and setup your db in one line of code:
[MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreNamed:#"Words"];

Resources