I'm using Callkit extension to identify the numbers. All my contacts (around 30k+) are stored in Realm.
I have stored the Realm file in AppGroup which can be shared between my app and its extensions.
I get the error when I try to reload the extension.
Error Domain=com.apple.CallKit.error.calldirectorymanager Code=7
"(null)"
When this error occurred , the setting for Call Blocking & Identification for my app shows a spinner (while other apps show the switch to toggle)
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[[CXCallDirectoryManager sharedInstance] reloadExtensionWithIdentifier:#"com.j2x.handheldcontact.CallerID" completionHandler:^(NSError *error){
if(error) {
NSLog(#"CallerID - refresh failed. error is %#",[error description]);
}
}];
}
I see that the error happens only when I try to use access the Realm in the app group directory.
In my extension subclass:
- (void)beginRequestWithExtensionContext:(CXCallDirectoryExtensionContext *)context
{
context.delegate = self;
NSString *appGroupId = #"group.com.j2x.handheldcontact.CallerID";
NSURL *appGroupDirectoryPath = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupId];
NSURL *dataBaseURL = [appGroupDirectoryPath URLByAppendingPathComponent:#"default.realm"];
[[[RLMRealm defaultRealm]configuration]setFileURL:dataBaseURL];
RLMResults *temp = [self getContactArray]; //This gives the callKit error
RLMResults *temp ; //This doesn't give any error
[context completeRequestWithCompletionHandler:nil];
}
-(RLMResults *)getContactArray{
RLMResults *res = [[RealmContact allObjects]objectsWithPredicate:[NSPredicate predicateWithFormat:#"phone <> nil or homePhone <> nil or mobilePhone <> nil or altPhone <> nil or fax <> nil"]];
return res;
}
Why does accessing the Realm data gives the error ? The predicate format does look ok to me.
With some research I found the following code:
public enum Code : Int {
public typealias _ErrorType = CXErrorCodeCallDirectoryManagerError
case unknown
case noExtensionFound
case loadingInterrupted
case entriesOutOfOrder
case duplicateEntries
case maximumEntriesExceeded
case extensionDisabled
#available(iOS 10.3, *)
case currentlyLoading
#available(iOS 11.0, *)
case unexpectedIncrementalRemoval
}
In my case , the error says case currentlyLoading (code 7). I also tried this on realm with only 250 contacts . But I got the same error.
Edit:
If I hardcode the contact, it works fine. But if I bring Realm into scene, it fails.
CXCallDirectoryPhoneNumber phoneNumber = strtoull([#"14xxxxxx86" UTF8String], NULL, 0);
if (phoneNumber > 0) {
[context addIdentificationEntryWithNextSequentialPhoneNumber:phoneNumber label:#"Test Test"];
}
Workaround:
For now, I'm storing all my data into a file and saving that file in the app group.
NSString *appGroupId = #"group.xxx.CallerID";
NSURL *appGroupDirectoryPath = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupId];
NSURL *appFile = [appGroupDirectoryPath URLByAppendingPathComponent:#"contacts.txt"];
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:[appFile path]];
if(exists) {
[[NSFileManager defaultManager]removeItemAtPath:[appFile path] error:nil];
}
[NSKeyedArchiver archiveRootObject:uniqueCallDirectory toFile:[appFile path]];
and accessing this array in the callID extension subclass.
The list you provide to addIdentificationEntryWithNextSequentialPhoneNumber must be ordered by ascending phone Number. Retrieve the list from realm in ascending order. (else it will break and keep the loading icon when you activate the extension)
I want to offer the users of my app the possibility to create a backup of the core data database, especially in case he switches to a new device etc.
How would I do that? Especially how can I reimport that file? I mean let's say he makes a backup copy of the database, then changes a ton of stuff and wants to reset to the previous saved backup copy. How would I do that?
Thx!
Take a look at this sample app, it includes functions for making backups, copying backups to and from iCloud, emailing backups and importing backups from email.
http://ossh.com.au/design-and-technology/software-development/sample-library-style-ios-core-data-app-with-icloud-integration/
BTW it's much safer to use migratePersistentStore API to make/import backups if your are doing so to and from ICloud. Also be aware that the sample app assumes you are not using WAL mode which is the default mode for iOS 7. WAL mode uses multiple files which all need to be backed up or copied.
Here is a link to a video demonstrating the sample Apps backup and restore capabilities.
http://ossh.com.au/design-and-technology/software-development/sample-library-style-ios-core-data-app-with-icloud-integration/sample-apps-explanations/backup-files/
Here are the methods used to create copies for backup. Note that it is possible to open the store with multiple persistentStoreCoordinators so no need to close it down while you make a backup. Restoring it does obviously require the existing store to be removed first. Note that there is little difference between the two methods below except that the source store is opened with or without iCloud options.
/*! Creates a backup of the ICloud store
#return Returns YES of file was migrated or NO if not.
*/
- (bool)backupICloudStore {
FLOG(#"backupICloudStore called");
// Lets use the existing PSC
NSPersistentStoreCoordinator *migrationPSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
// Open the store
id sourceStore = [migrationPSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self icloudStoreURL] options:[self icloudStoreOptions] error:nil];
if (!sourceStore) {
FLOG(#" failed to add old store");
migrationPSC = nil;
return FALSE;
} else {
FLOG(#" Successfully added store to migrate");
NSError *error;
FLOG(#" About to migrate the store...");
id migrationSuccess = [migrationPSC migratePersistentStore:sourceStore toURL:[self backupStoreURL] options:[self localStoreOptions] withType:NSSQLiteStoreType error:&error];
if (migrationSuccess) {
FLOG(#"store successfully backed up");
migrationPSC = nil;
// Now reset the backup preference
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:_makeBackupPreferenceKey];
[[NSUserDefaults standardUserDefaults] synchronize];
return TRUE;
}
else {
FLOG(#"Failed to backup store: %#, %#", error, error.userInfo);
migrationPSC = nil;
return FALSE;
}
}
migrationPSC = nil;
return FALSE;
}
/*! Creates a backup of the Local store
#return Returns YES of file was migrated or NO if not.
*/
- (bool)backupLocalStore {
FLOG(#"backupLocalStore called");
// Lets use the existing PSC
NSPersistentStoreCoordinator *migrationPSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
// Open the store
id sourceStore = [migrationPSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self localStoreURL] options:[self localStoreOptions] error:nil];
if (!sourceStore) {
FLOG(#" failed to add old store");
migrationPSC = nil;
return FALSE;
} else {
FLOG(#" Successfully added store to migrate");
NSError *error;
FLOG(#" About to migrate the store...");
id migrationSuccess = [migrationPSC migratePersistentStore:sourceStore toURL:[self backupStoreURL] options:[self localStoreOptions] withType:NSSQLiteStoreType error:&error];
if (migrationSuccess) {
FLOG(#"store successfully backed up");
migrationPSC = nil;
// Now reset the backup preference
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:_makeBackupPreferenceKey];
[[NSUserDefaults standardUserDefaults] synchronize];
return TRUE;
}
else {
FLOG(#"Failed to backup store: %#, %#", error, error.userInfo);
migrationPSC = nil;
return FALSE;
}
}
migrationPSC = nil;
return FALSE;
}
/** Sets the selected file as the current store.
Creates a backup of the current store first.
#param fileURL The URL for the file to use.
*/
- (BOOL)restoreFile:(NSURL *)fileURL {
FLOG(#" called");
// Check if we are using iCloud
if (_isCloudEnabled) {
FLOG(#" using iCloud store so OK to restore");
NSURL *currentURL = [self storeURL];
FLOG(#" currentURL is %#", currentURL);
FLOG(#" URL to use is %#", fileURL);
[self saveContext];
[self backupCurrentStoreWithNoCheck];
// Close the current store and delete it
_persistentStoreCoordinator = nil;
_managedObjectContext = nil;
[self removeICloudStore];
[self moveStoreFileToICloud:fileURL delete:NO backup:NO];
} else {
FLOG(#" using local store so OK to restore");
NSURL *currentURL = [self storeURL];
FLOG(#" currentURL is %#", currentURL);
FLOG(#" URL to use is %#", fileURL);
[self saveContext];
[self backupCurrentStoreWithNoCheck];
// Close the current store and delete it
_persistentStoreCoordinator = nil;
_managedObjectContext = nil;
NSError *error = nil;
NSFileManager *fm = [[NSFileManager alloc] init];
// Delete the current store file
if ([fm fileExistsAtPath:[currentURL path]]) {
FLOG(#" target file exists");
if (![fm removeItemAtURL:currentURL error:&error]) {
FLOG(#" error unable to remove current store file");
NSLog(#"Error removing item Error: %#, %#", error, error.userInfo);
return FALSE;
} else {
FLOG(#" current store file removed");
}
}
//
//simply copy the file over
BOOL copySuccess = [fm copyItemAtPath:[fileURL path]
toPath:[currentURL path]
error:&error];
if (copySuccess) {
FLOG(#" replaced current store file successfully");
//[self postFileUpdateNotification];
} else {
FLOG(#"Error copying items Error: %#, %#", error, error.userInfo);
return FALSE;
}
}
// Now open the store again
[self openPersistentStore];
return TRUE;
}
Whatever the persistent store is that you use (binary, SQLite, etc.); it is just a file on the filesystem. You can make a copy of it whenever you want.
If you are using SQLite in iOS 7, be sure to make a copy of the other files associated with it as they are the journal files that go with it. If you are using binary then there will be only a single file.
If you just copy the file there is no import step, you just copy it back to restore it.
There are more advanced designs such as exporting the entire database to something that is portable, such as JSON but that is a different subject.
Update
Well I've used the standard Xcode core data template, so according to the code I've just checked I'm using SQLite. So how do I find all related files? Or could you show me with some example code how to copy and insert back the files needed?
You use NSFileManager to copy the files. You can look at the documents directory in your iOS simulator application to see the names of all the files. Or you could use NSFileManager to scan the documents directory, find everything that starts with the same file name (MyData.* for example) and copy that into a back up directory.
As for sample code, no; it is only a couple of lines of code once you look at the documentation for NSFileManager.
I created the following method with the help of Apple sample code. This will take a backup of core data files and place it to the path that you want.
Swift 5
/// Backing up store type to a new and unique location
/// The method is illustrated in the following code fragment, which shows how you can use migratePersistentStore to take a back up of a store and save it from one location to another.
/// If the old store type is XML, the example also converts the store to SQLite.
/// - Parameters:
/// - path: Where you want the backup to be done, please create a new unique directory with timestamp or the guid
/// - completion: Passes error in case of error or pass nil in case of success
class func backUpCoreDataFiles(path : URL, completion : #escaping (_ error : String?) -> ())
{
// Every time new container is a must as migratePersistentStore method will loose the reference to the container on migration
let container = NSPersistentContainer(name : "<YourDataModelName>")
container.loadPersistentStores
{ (storeDescription, error) in
if let error = error
{
fatalError("Failed to load store: \(error)")
}
}
let coordinator = container.persistentStoreCoordinator
let store = coordinator.persistentStores[0]
do
{
try coordinator.migratePersistentStore(store, to : path, options : nil, withType : NSSQLiteStoreType)
completion(nil)
}
catch
{
completion("\(Errors.coredataBackupError)\(error.localizedDescription)")
}
}
I have an app that's using background downloads with the new NSURLSession APIs. When a download cancels or fails in such a way that NSURLSessionDownloadTaskResumeData is provided, I store the data blob so that it can be resumed later. A very small amount of the time I am noticing a crash in the wild:
Fatal Exception: NSInvalidArgumentException
Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file.
The error occurs here, where resumeData is the NSData blob and session is an instance of NSURLSession:
if (resumeData) {
downloadTask = [session downloadTaskWithResumeData:resumeData];
...
The data is provided by the Apple APIs, is serialized, and is then deserialized at a later point in time. It may be corrupted, but it is never nil (as the if statement checks).
How can I check ahead of time that the resumeData is invalid so that I do not let the app crash?
This is the workaround suggested by Apple:
- (BOOL)__isValidResumeData:(NSData *)data{
if (!data || [data length] < 1) return NO;
NSError *error;
NSDictionary *resumeDictionary = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:&error];
if (!resumeDictionary || error) return NO;
NSString *localFilePath = [resumeDictionary objectForKey:#"NSURLSessionResumeInfoLocalPath"];
if ([localFilePath length] < 1) return NO;
return [[NSFileManager defaultManager] fileExistsAtPath:localFilePath];
}
Edit (iOS 7.1 is not NDA'd anymore): I got this from a Twitter exchange with an Apple engineer, he suggested what to do, and I wrote the above implementation
I have not found an answer to how to tell if the data is valid ahead of time.
However, I am presently working around the issue like so:
NSData *resumeData = ...;
NSURLRequest *originalURLRequest = ...;
NSURLSessionDownloadTask *downloadTask = nil;
#try {
downloadTask = [session downloadTaskWithResumeData:resumeData];
}
#catch (NSException *exception) {
if ([NSInvalidArgumentException isEqualToString:exception.name]) {
downloadTask = [session downloadTaskWithRequest:originalURLRequest];
} else {
#throw exception; // only swallow NSInvalidArgumentException for resumeData
}
}
actually, the resume data is a plist file.
it contains the follows key:
NSURLSessionDownloadURL
NSURLSessionResumeBytesReceived
NSURLSessionResumeCurrentRequest
NSURLSessionResumeEntityTag
NSURLSessionResumeInfoTempFileName
NSURLSessionResumeInfoVersion
NSURLSessionResumeOriginalRequest
NSURLSessionResumeServerDownloadDate
so the steps u need to do are:
check the data is a valid plist;
check the plist have keys as above;
check the temp file is exist;
Perplexing quandary: When I get my friends list with one URL I get the list back fine, using the same URL but with a query string (?fields=id,name,picture), and the exact same permissions, I get an error indicating "An active access token must be used to query information about the current user." What gives?
The permissions currently in effect are publish_stream, email, and read_stream. Why would adding that query string mess it up? The only thing I can think of is that the access key I have is not what I think. Is there a way to pull the actual access key out, expose it in an NSLog, and then test it on the graph explorer?
The URL that works is:
https://graph.facebook.com/me/friends
The URL that doesn't is:
https://graph.facebook.com/me/friends?fields=id,name,picture
This is the code that actually get the permissions. In fact this is the same code that Stuart Breckenridge offered up freely on GitHub (thanks dude!) It seems to work fine as long as I am not appending '?fields=name,id,picture' to the end of the api call:
-(void)requestPermissions
{
if (debugF) NSLog(#"FAM: requestPermissions");
// Specify the Facebook App ID.
_facebookAppID = #"123456789123456"; // You Must Specify Your App ID Here.
// Submit the first "read" request.
// Note the format of the facebookOptions dictionary. You are required to pass these three keys: ACFacebookAppIdKey, ACFacebookAudienceKey, and ACFacebookPermissionsKey
// Specify the read permission
_facebookPermissions = #[#"email"];
// Create & populate the dictionary the dictionary
_facebookOptions = #{ ACFacebookAppIdKey : _facebookAppID,
ACFacebookAudienceKey : ACFacebookAudienceFriends,
ACFacebookPermissionsKey : _facebookPermissions};
_facebookAccountType = [_facebookAccountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];
[_facebookAccountStore requestAccessToAccountsWithType:_facebookAccountType options:_facebookOptions completion:^(BOOL granted, NSError *error)
{
// If read permission are granted, we then ask for write permissions
if (granted) {
_readPermissionsGranted = YES;
// We change the _facebookOptions dictionary to have a publish permission request
_facebookPermissions = #[#"publish_stream", #"read_stream", #"friends_photos"];
_facebookOptions = #{ ACFacebookAppIdKey : _facebookAppID,
ACFacebookAudienceKey : ACFacebookAudienceFriends,
ACFacebookPermissionsKey : _facebookPermissions};
[_facebookAccountStore requestAccessToAccountsWithType:_facebookAccountType options:_facebookOptions completion:^(BOOL granted2, NSError *error)
{
if (granted2)
{
_publishPermissionsGranted = YES;
// Create the facebook account
_facebookAccount = [[ACAccount alloc] initWithAccountType:_facebookAccountType];
_arrayOfAccounts = [_facebookAccountStore accountsWithAccountType:_facebookAccountType];
_facebookAccount = [_arrayOfAccounts lastObject];
}
// If permissions are not granted to publish.
if (!granted2)
{
if (debugF) NSLog(#"Publish permission error: %#", [error localizedDescription]);
_publishPermissionsGranted = NO;
}
}];
}
// If permission are not granted to read.
if (!granted)
{
if (debugF) NSLog(#"Read permission error: %#", [error localizedDescription]);
_readPermissionsGranted = NO;
if ([[error localizedDescription] isEqualToString:#"The operation couldn’t be completed. (com.apple.accounts error 6.)"])
{
[self performSelectorOnMainThread:#selector(showError) withObject:error waitUntilDone:NO];
}
}
}];
}
As it turns out, the answer to my original question was simpler than I thought:
NSString *acessToken = [NSString stringWithFormat:#"%#", self.facebookAccount.credential.oauthToken];
Kudos to all who contributed to the conversation, however.
I've been creating a list app and backing it with core data.
I would like to have a default list of say 10 airport's items, so that the user doesn't have to start from scratch.
Is there any way to do this?
Any help is appreciated.
Thanks in advance.
Here's the best way (and doesn't require SQL knowledge):
Create a quick Core Data iPhone app (Or even Mac app) using the same object model as your List app. Write a few lines of code to save the default managed objects you want to the store. Then, run that app in the simulator. Now, go to ~/Library/Application Support/iPhone Simulator/User/Applications. Find your application among the GUIDs, then just copy the sqlite store out into your List app's project folder.
Then, load that store like they do in the CoreDataBooks example.
Yes there is in fact the CoreDataBooks example does this, you can download the code here: sample code
What you do is create the internal store (database) using the normal procedure to initialize your store just like you would with any other store, then you simply run your code and let it execute the code as described in the CoreDataBooks example (code snippet below). Once the store has been initialized you will want to create a NSManagedObjectContext and initialize it with the created persistent store, insert all the entities you need, and save the context.
Once the context has been successfully saved, you can stop your application, then go to finder and go to folder: ~/Library/Developer type in the search .sqlite and look under /Developer, sorting by date will give you the most recent .sqlite database which should match the time that the code was executed, you can then take this store and add it as a resource of your project. This file then can be read by a persistent store coordinator.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator) {
return persistentStoreCoordinator;
}
NSString *storePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent: #"CoreDataBooks.sqlite"];
/*
Set up the store.
For the sake of illustration, provide a pre-populated default store.
*/
NSFileManager *fileManager = [NSFileManager defaultManager];
// If the expected store doesn't exist, copy the default store.
if (![fileManager fileExistsAtPath:storePath]) {
NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:#"CoreDataBooks" ofType:#"sqlite"];
if (defaultStorePath) {
[fileManager copyItemAtPath:defaultStorePath toPath:storePath error:NULL];
}
}
NSURL *storeUrl = [NSURL fileURLWithPath:storePath];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
NSError *error;
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
return persistentStoreCoordinator;
}
Hope that helps.
-Oscar
With this method you don't need to make a separate app or have any SQL knowledge. You only need to be able to make a JSON file for your initial data.
I use a JSON file that I parse into objects, then insert them in Core Data. I do this when the app initializes. I also make one entity in my core data that indicates if this initial data is already inserted, after I insert the initial data I set this entity so the next time the script runs it sees that the initial data has already been initialized.
To read json file into objects:
NSString *initialDataFile = [[NSBundle mainBundle] pathForResource:#"InitialData" ofType:#"json"];
NSError *readJsonError = nil;
NSArray *initialData = [NSJSONSerialization
JSONObjectWithData:[NSData dataWithContentsOfFile:initialDataFile]
options:kNilOptions
error:&readJsonError];
if(!initialData) {
NSLog(#"Could not read JSON file: %#", readJsonError);
abort();
}
Then you can make entity objects for it like this:
[initialData enumerateObjectsUsingBlock:^(id objData, NSUInteger idx, BOOL *stop) {
MyEntityObject *obj = [NSEntityDescription
insertNewObjectForEntityForName:#"MyEntity"
inManagedObjectContext:dataController.managedObjectContext];
obj.name = [objData objectForKey:#"name"];
obj.description = [objData objectForKey:#"description"];
// then insert 'obj' into Core Data
}];
If you want a more detailed description on how to do this, check out this tutorial:
http://www.raywenderlich.com/12170/core-data-tutorial-how-to-preloadimport-existing-data-updated
For 10 items, you can just do this within applicationDidFinishLaunching: in your app delegate.
Define a method, say insertPredefinedObjects, that creates and populates the instances of the entity in charge of managing your airport items, and save your context. You may either read the attributes from a file or simply hardwire them in your code. Then, call this method inside applicationDidFinishLaunching:.
Bear in mind, when following the CoreDataBooks example code, that it probably breaks the iOS Data Storage Guidelines:
https://developer.apple.com/icloud/documentation/data-storage/
I've had an app rejected for copying the (read-only) pre-populated database to the documents directory - as it then gets backed up to iCloud - and Apple only want that to happen to user-generated files.
The guidelines above offer some solutions, but they mostly boil down to:
store the DB in the caches directory, and gracefully handle situations where the OS purges the caches - you will have to rebuild the DB, which probably rules it out for most of us.
set a 'do not cache attribute' on the DB file, which is a little arcane, as it needs to be done differently for different OS versions.
I don't think it is too tricky, but be aware that you have a bit extra to do to make that example code work alongside iCloud...
This answer is only for people who are
including a pre-populated database in your app
making an app for multiple platforms (iOS, Android, etc.)
I had made a prepopulated SQLite database for an Android app. Then when I was making an iOS version of the app I thought it would be best to use Core Data. So I spent quite a long time learning Core Data and then rewriting the code to prepopulate the database. Learning how to do every single step in both platforms required lots of research and trial and error. There was a lot less overlap than I would have hoped.
In the end I just decided to use the same SQLite database from my Android project. Then I used the FMDB wrapper to directly access the database in iOS. The benefits:
Only need to make the prepopulated database once.
Doesn't require a paradigm shift. The syntax between Android and FMDB, while different, is still fairly similar.
Have a lot more control over how Queries are performed.
Allows full text search.
Although I don't regret learning Core Data, if I were to do it over I could have saved a lot of time by just sticking to SQLite.
If you are starting in iOS and then planning to move to Android, I would still use a SQLite wrapper like FMDB or some other software to prepopulate the database. Although you can technically extract the SQLite database that you prepopulate with Core Data, the schema (table and column names, etc.) will be strangely named.
By the way, if you don't need to modify your prepopulated database, then don't copy it to the documents directory after the app is installed. Just access it directly from the bundle.
// get url reference to databaseName.sqlite in the bundle
let databaseURL: NSURL = NSBundle.mainBundle().URLForResource("databaseName", withExtension: "sqlite")!
// convert the url to a path so that FMDB can use it
let database = FMDatabase(path: databaseURL.path)
This makes it so that you don't have two copies.
Update
I now use SQLite.swift rather than FMDB, because it integrates better with Swift projects.
This worked for me. This is a modification of this answer by Andrea Toso and inspired by this blog. The only issue with the answer is that there is a chance of data loss when moving sqlite files with FileManager. I saved around 500 rows of data by using replacePersistentStore instead of FileManager.default.copyItem
Step 1
Populate your Core Data in another app and get files' path using this code:
let paths = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
print(documentsDirectory)
Step2
Drag your 3 files with .sqlite extension into your xCode project. (Be sure to select Add to targets option).
Step3
Create function to check app's first run in AppDelegate.swift
func isFirstLaunch() -> Bool {
let hasBeenLaunchedBeforeFlag = "hasBeenLaunchedBeforeFlag"
let isFirstLaunch = !UserDefaults.standard.bool(forKey: hasBeenLaunchedBeforeFlag)
if (isFirstLaunch) {
UserDefaults.standard.set(true, forKey: hasBeenLaunchedBeforeFlag)
UserDefaults.standard.synchronize()
}
return isFirstLaunch
}
Step4
Copy this function in AppDelegate.swift to get url where sqlite database should be moved:
func getDocumentsDirectory()-> URL {
let paths = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
Step 5
Replace declaration of persistentContainer with this one:
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "ProjectName")
let storeUrl = self.getDocumentsDirectory().appendingPathComponent("FileName.sqlite")
if isFirstLaunch() {
let seededDataUrl = Bundle.main.url(forResource: "FileName", withExtension: "sqlite")
try! container.persistentStoreCoordinator.replacePersistentStore(at: storeUrl, destinationOptions: nil, withPersistentStoreFrom: seededDataUrl!, sourceOptions: nil, ofType: NSSQLiteStoreType)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
So I have developed a generic method that loads from a dictionary (possibly from JSON) and populates the database.
It should be used ONLY with trusted data (from a safe channel), it can't handle circular references and schema migrations can be problematic... But for simple use cases like mine it should be fine
Here it goes
- (void)populateDBWithDict:(NSDictionary*)dict
withContext:(NSManagedObjectContext*)context
{
for (NSString* entitieName in dict) {
for (NSDictionary* objDict in dict[entitieName]) {
NSManagedObject* obj = [NSEntityDescription insertNewObjectForEntityForName:entitieName inManagedObjectContext:context];
for (NSString* fieldName in objDict) {
NSString* attName, *relatedClass, *relatedClassKey;
if ([fieldName rangeOfString:#">"].location == NSNotFound) {
//Normal attribute
attName = fieldName; relatedClass=nil; relatedClassKey=nil;
} else {
NSArray* strComponents = [fieldName componentsSeparatedByString:#">"];
attName = (NSString*)strComponents[0];
relatedClass = (NSString*)strComponents[1];
relatedClassKey = (NSString*)strComponents[2];
}
SEL selector = NSSelectorFromString([NSString stringWithFormat:#"set%#:", attName ]);
NSMethodSignature* signature = [obj methodSignatureForSelector:selector];
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:obj];
[invocation setSelector:selector];
//Lets set the argument
if (relatedClass) {
//It is a relationship
//Fetch the object
NSFetchRequest* query = [NSFetchRequest fetchRequestWithEntityName:relatedClass];
query.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:relatedClassKey ascending:YES]];
query.predicate = [NSPredicate predicateWithFormat:#"%K = %#", relatedClassKey, objDict[fieldName]];
NSError* error = nil;
NSArray* matches = [context executeFetchRequest:query error:&error];
if ([matches count] == 1) {
NSManagedObject* relatedObject = [matches lastObject];
[invocation setArgument:&relatedObject atIndex:2];
} else {
NSLog(#"Error! %# = %# (count: %d)", relatedClassKey,objDict[fieldName],[matches count]);
}
} else if ([objDict[fieldName] isKindOfClass:[NSString class]]) {
//It is NSString
NSString* argument = objDict[fieldName];
[invocation setArgument:&argument atIndex:2];
} else if ([objDict[fieldName] isKindOfClass:[NSNumber class]]) {
//It is NSNumber, get the type
NSNumber* argument = objDict[fieldName];
[invocation setArgument:&argument atIndex:2];
}
[invocation invoke];
}
NSError *error;
if (![context save:&error]) {
NSLog(#"%#",[error description]);
}
}
}
}
And loads from json...
NSString *filePath = [[NSBundle mainBundle] pathForResource:#"initialDB" ofType:#"json"];
NSData *jsonData = [NSData dataWithContentsOfFile:filePath];
NSError* error;
NSDictionary *initialDBDict = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers error:&error];
[ self populateDBWithDict:initialDBDict withContext: [self managedObjectContext]];
JSON examples
{
"EntitieA": [ {"Att1": 1 }, {"Att1": 2} ],
"EntitieB": [ {"Easy":"AS ABC", "Aref>EntitieA>Att1": 1} ]
}
and
{
"Country": [{"Code": 55, "Name": "Brasil","Acronym": "BR"}],
"Region": [{"Country>Country>code": 55, "Code": 11, "Name": "Sao Paulo"},
{"Country>Country>code": 55, "Code": 31, "Name": "Belo Horizonte"}]
}
How about check if any objects exist and if not, create one with some data?
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Settings"];
_managedObjectSettings = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
if ([_managedObjectSettings count] == 0) {
// first time, create some defaults
NSManagedObject *newDevice = [NSEntityDescription insertNewObjectForEntityForName:#"Settings" inManagedObjectContext:managedObjectContext];
[newDevice setValue:[NSNumber numberWithBool: YES ] forKey:#"speed"];
[newDevice setValue:[NSNumber numberWithBool: YES ] forKey:#"sound"];
[newDevice setValue:[NSNumber numberWithBool: NO ] forKey:#"aspect"];
[newDevice setValue:[NSNumber numberWithBool: NO ] forKey: #"useH264"];
[newDevice setValue:[NSNumber numberWithBool: NO ] forKey: #"useThumbnail"];
NSError *error = nil;
// Save the object to persistent store
if (![managedObjectContext save:&error]) {
NSLog(#"Can't Save! %# %#", error, [error localizedDescription]);
}
}
Another method for storing defaults is found by way of NSUserDefaults. (surprise!)
And its easy.
Suggested by some, put that into the applicationDidFinishLaunching
In the given case of 10 defaults, Airport0 thru 9
Setting
NSUserDefaults *nud = [NSUserDefaults standardUserDefaults];
[nud setString:#"MACADDRESSORWHY" forKey:#"Airport0"];
...
[nud setString:#"MACADDRESSORWHY" forKey:#"Airport9"];
[nud synchronize];
or
[[NSUserDefaults standardUserDefaults] setString:#"MACADDRESSORWHY" forKey:#"Airport9"]];
...
[[NSUserDefaults standardUserDefaults] synchronize];
And then, getting the defaults.
NSString *air0 = [[NSUserDefaults standardUserDefaults] stringForKey:#"Airport0"];
As most answers are quite old, I recommend the following tutorial. It explains how it can be done.
https://www.youtube.com/watch?v=xcV8Ow9nWFo