Shipping core data with iOS app - ios

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.

Related

Bundle.main does not find file in folder structure

Overview
I want to find my core data model as part of the initialisation of NSPersistentContainers (local and cloud) to make sure that they share the same model.
Code
The code I planned to use to achieve this:
static let model: NSManagedObjectModel = {
guard let url = Bundle.main.url(forResource: "BookKeeping", withExtension: "momd") else {
fatalError("Failed to locate model file.")
}
guard let managedObjectModel = NSManagedObjectModel(contentsOf: url) else {
fatalError("Failed to load model file.")
}
return managedObjectModel
}()
However this does not do trick, as it fails with the first fatalError "Failed to locate model file".
I presume it has something to do with my folder structure, which is the following:
Folder Structure
In "Persistence" the initialisation takes places, "BookKeeping" is the name of the model file.
Approaches so far
I tried to add the file as Bundle Resource in the Build Phase, I checked that the target membership of the file is right, FileManager is in my opinion not the right approach to get it done and I looked into Bundle identifiers, however it seems to me they are used in different use cases.
If you could put me in the right direction I'd be happy.
Any help is greatly appreciated! Thanks!

An error occurring during Core Data persistent store migration in iOS 13

After updating XCode to version 11 I added a new model version to Core Data and in new version I added a new attribute to an Entity. Made the new version active and added the new property to managed object file.
After releasing this version to the users it started to crash with the following message: "The managed object model version used to open the persistent store is incompatible with the one that was used to create the persistent store." and "duplicate column name ZNEWCOLUMN". Until now I made a lot of changes to the Core Data model and migration always worked.
This crash appears only on iOS 13!
This is how I load Core Data:
lazy var managedObjectContext: NSManagedObjectContext = {
return self.persistentContainer.viewContext
}()
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "MyModel")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() 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.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions.append(description)
return container
}()
Any help would be appreciated.
The same thing is happening to me, lightweight migration at iOS 12 was right at real device and Simulator but at iOS 13 fail with the next log result:
SQLite error code:1, 'duplicate column name: ZNAME_OF_THE_COLUMN .... Error Domain
= NSCocoaErrorDomain Code = 134110 "An error occurred during persistent storage migration."
I load data like #iOS Dev post.
I check the xxxx.sqlite database file in the emulator path before and after the migration and there were no columns with those new same names.
To know the route of the *.sqlite in emulator you have to put a breakpoint and when it is stopped put in the console po NSHomeDirectory().
Then go to Finder window, tap the keys Control + Command + G and paste the route. Yo can handle it (for example) with DB Browser for SQLite program, it´s free.
After a long search I have seen what has happened to some people but I have not seen any solution.
Mine was:
Select the actual *.xcdatamodel.
Select Editor > Add Model Version.
Provide a version name based on the previous model (like XxxxxxV2.xcdatamodel).
Click on this new version model NewV2.xcdatamodel.
Select this new version as Current on Properties at right hand of IDE.
Make your changes at DDBB.
Run app and will work fine.
I did tests overriding app (with new values) and it was fine.
I hope this may help.
If you want to edit the descriptions, you need to do so before you load the stores (and I have no idea what appending a new description would do):
container.persistentStoreDescriptions.forEach { storeDesc in
storeDesc.shouldMigrateStoreAutomatically = true
storeDesc.shouldInferMappingModelAutomatically = true
}
container.loadPersistentStores { [unowned self] (storeDesc, error) in
if let error = error {
// handle your error, do not fatalError! even a message that something is wrong can be helpful
return
}
// do any additional work on your view context, etc.
}
If your problem is reproduceable, you should look at the error that's being returned and look for something called ZNEWCOLUMN (though this sounds like a temporary default name?) This nomenclature is the raw column name in the SQL database though, so it's likely the migrator is attempting to add this new column and failing.
Try turning on SQL debugging in your scheme's Arguments:
-com.apple.CoreData.SQLDebug 1
Try logging into the raw SQL database (the above will give you the raw path if you're on the simulator). Try rolling back to the previous data model on a previous OS and then just upgrading to 13.
Sounds like you have some duplicate column somewhere so these are just some ideas to find out where it is.

Unable to open a realm at path

I am trying to use a bundled realm file without success. I know that my realm file was copied successfully to my application’s Directory but I ca not read it.
fatal error: 'try!' expression unexpectedly raised an error: "Unable
to open a realm at path
'/Users/…/Library/Developer/CoreSimulator/Devices/…/data/Containers/Data/Application/…/Documents/default-v1.realm'.
Please use a path where your app has read-write permissions."
func fullPathToFileInAppDocumentsDir(fileName: String) -> String {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,NSSearchPathDomainMask.UserDomainMask,true)
let documentsDirectory = paths[0] as NSString
let fullPathToTheFile = documentsDirectory.stringByAppendingPathComponent(fileName)
return fullPathToTheFile
}
In didFinishLaunchingWithOptions:
let fileInDocuments = fullPathToFileInAppDocumentsDir("default-v1.realm")
if !NSFileManager.defaultManager().fileExistsAtPath(fileInDocuments) {
let bundle = NSBundle.mainBundle()
let fileInBundle = bundle.pathForResource("default-v1", ofType: "realm")
let fileManager = NSFileManager.defaultManager()
do {
try fileManager.copyItemAtPath(fileInBundle!, toPath: fileInDocuments)
} catch { print(error) }
}
And setting the configuration used for the default Realm:
var config = Realm.Configuration()
config.path = fileInDocuments
Realm.Configuration.defaultConfiguration = config
let realm = try! Realm(configuration: config) // It fails here!!! :-)
As the documentation suggests, I have tried as well to open it directly from the bundle path by setting readOnly to true on the Realm.Configuration object. I am not sure if this is something related to Realm or if I am overlooking something with the file system…. I have also tried to store the file in the Library folder.
Realm 0.97.0
Xcode Version 7.1.1
I tried to open the realm file using Realm's browser app and the file does not open anymore. It has now new permissions: Write only (Dropbox). So, I decided to change the file permission back to read/write using file manager’s setAttributes method. Here is how I did it:
// rw rw r : Attention for octal-literal in Swift "0o".
let permission = NSNumber(short: 0o664)
do {
try fileManager.setAttributes([NSFilePosixPermissions:permission], ofItemAtPath: fileInDocuments)
} catch { print(error) }
The realm file can now be open at this path.
That exception gets thrown whenever a low level I/O operation is denied permission to the file you've specified (You can check it out on Realm's GitHub account).
Even though everything seems correct in your sample code there, something must be set incorrectly with the file location (Whether it be the file path to your bundle's Realm file, or the destination path) to be causing that error.
A couple of things I can recommend trying out.
Through breakpoints/logging, manually double-check that both fileInDocuments and fileInBundle are being correctly created and are pointing at the locations you were expecting.
When running the app in the Simulator, use a tool like SimPholders to track down the Documents directory of your app on your computer, and visually ensure that the Realm file is being properly copied to where you're expecting.
If the Realm file is in the right place, you can also Realm's Browser app to try opening the Realm file to ensure that the file was copied correctly and still opens properly.
Try testing the code on a proper iOS device to see if the same error is occurring on the native system.
If all else fails, try doing some Realm operations with the default Realm (Which will simply deposit a default.realm file in your Documents directory), just to completely discount there isn't something wrong with your file system
Let me know how you go with that, and if you still can't work out the problem, we can keep looking. :)
This will occur if you have your realm file open in Realm Studio at same time you relaunch your app. Basically in this case Realm can't get write permissions if Studio already has them.
To add to the solution based on what I've discovered, make note of what error Realm reports when it throws the exception, as well as the type of Error that is passed.
As of writing, Realm documents their errors here:
https://realm.io/docs/objc/latest/api/Enums/RLMError.html
What this means is that you can find out if your Realm file has permissions problems and react to those based on Realm passing you a RLMErrorFilePermissionDenied. Or if the file doesn't exist with RLMErrorFileNotFound.
The tricky thing I'm finding is when you get a more generic RLMErrorFileAccess, but that's for another question on Stack Overflow...
I had the same issue and tried too many ways to fix it. The easiest way to fix this problem is manually creation of the folder XCode cannot reach '/Users/…/Library/Developer/CoreSimulator/Devices/…/data/Containers/Data/Application/…/Documents/...' as explained at https://docs.realm.io/sync/using-synced-realms/errors#unable-to-open-realm-at-path-less-than-path-greater-than-make_dir-failed-no-such-file-or-directory
Once you created this folder and run the project, XCode creates the Realm files inside this folder automatically.

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

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.

iOS Adding Initial Core Data file using UIManagedDocument, NOT appdelegate

I am trying to load an initial database into my app so my core data db is not empty upon install. I'm now using this code:
NSFileManager *fileManager = [NSFileManager defaultManager];
// If the expected store doesn't exist, copy the default store.
if (![fileManager fileExistsAtPath:storePath]) {
NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:#"%#_InitialData", SQL_DATABASE_NAME] ofType:#"sqlite"];
if (defaultStorePath) {
[fileManager copyItemAtPath:defaultStorePath toPath:storePath error:NULL];
}
}
(from http://code.google.com/p/coredatalibrary/wiki/LoadingInitialData)
to try and load an initial sqlite file for my core data to use. It isn't working and my program differs from the type used in that link in a few ways.
It appears the tutorial uses a file created to "use core data", which I am not. I just didn't happen to learn it that way (watched the stanford cs193p videos) and instead I'm using UIManagedDocument and performing this code in my top view controller. Because of this, I've run into a few problems.
I loaded up my app to create the initial data base so I could save the file to use for initial values. Upon doing so, I found that the way things are saved are different from in the tutorial. For example, if my url for my UIManagedDocument is .../Documents/Test , then my database file is .../Documents/Test/Store Content/persistentStore, where "persistentStore" is the database file. For one thing, a "Store Content" directory has been added. In addition, the sqlite file is named persistentStore and has no file extension. When I open the file it says
SQLite format 3���# �����������������������������������������������������������������-‚%
on the top though (I'm not familiar with SQLite or any db format for that matter but I assume this means it is an sqlite file).
I save this "persistentStore" file to use to load into my app using the code above. Upon doing so, I found that copyItemAtPath:toPath:error: will not copy as I expect. For example, if the storePath is .../Documents/Test , then my sqlite file that I'm copying over becomes renamed to Test.sqlite and is located at .../Documents/ instead of copying my file to a location of .../Documents/Test/.
Because of this, when I try to open the UIManagedDocument at that url (.../Documents/Test) I get this error
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:
'UIManagedDocument can only read documents that are file packages'
I've tried creating a directory to mimic the one created by core data before copying over my intial data (that is, I create the directories to have a path of .../Documents/Test/Store Content/ and then copy my initial data to be in the "Store Content" folder with a name of persistentStore) but that also doesn't work. UIManagedDocument can't open the document.
So how can I load in initial values to my core data db without having a project that is set to "use core data"? I have the (presumably) sqlite file with the initial data (when I open it and skim it it appears to have my initial values), so I just need to know how to copy it over properly so that I can still used UIManagedDocument to open the document and save via the UIManagedObjectContext.
It turns out my way of recreating the path created by core data worked. That is, creating the directories of .../Test/Store Content/ and copying the file as "persistentStore". My error was a small one. It was suppose to be StoreContent instead of "Store Content"; I added a space...

Resources