Preload sqlite to core data - swift

I am trying to preload data to core data at the first run with a sqlite file. I first created a xcdatamodeld file, and then use python to produce the sqlite database, which has exactly the same data structure with the xcdatamodeld I stated.
#main
struct ArgosApp: App {
static var isFirstLaunch = isAppAlreadyLaunchedOnce()
// MARK:- Initialise Core Data Stack
var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Argos")
// if it is, load data from sql
if ArgosApp.isFirstLaunch {
guard let seededDataURL = Bundle.main.url(forResource: "karate", withExtension: "sqlite") else {
fatalError("Fail to find")
}
let storeUrl = ArgosApp.getDocumentsDirectory().appendingPathComponent("karate.sqlite")
if !FileManager.default.fileExists(atPath: (storeUrl.path)) {
try! FileManager.default.copyItem(at: seededDataURL, to: storeUrl)
}
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
description.url = storeUrl
description.type = "sqlite"
container.persistentStoreDescriptions = [description]
// ERROR came in here: Thread 1: "Unsupported store type."
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}
// else, load from core data
else {
container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
// TODO:- More decent error handling
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
return container
}()
var body: some Scene {
WindowGroup {
let context = persistentContainer.viewContext
let categories = Category.fetchAllCategories(context: context) ?? []
// Pass the persistent container to the view as environment object
ChallengeChooser(categories: categories)
.environment(\.managedObjectContext
,context)
}
}
// Detect if it's the first-launch
static func isAppAlreadyLaunchedOnce() -> Bool {
let defaults = UserDefaults.standard
if let _ = defaults.string(forKey: "isAppAlreadyLaunchedOnce") {
print("App already launched")
return false
} else {
defaults.set(true, forKey: "isAppAlreadyLaunchedOnce")
print("App launched first time")
return true
}
}
func saveContext (context: NSManagedObjectContext) {
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
static func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
}
The error came in when I tried to load the database saying
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unsupported store type.'
Here is the code I ran with sqlite3, I add some test data on it so the difficulty and explanation column are all NULL.
cur.execute("""
CREATE TABLE category (
name VARCHAR(40) PRIMARY KEY
);
""")
cur.execute("""
CREATE TABLE video (
url VARCHAR(100) PRIMARY KEY,
explanation VARCHAR(300),
difficulty INT,
category VARCHAR(40),
FOREIGN KEY(category) REFERENCES category(name)
ON DELETE CASCADE
ON UPDATE CASCADE
);
""")
And this is what my xcdatamodeld looks like, and I set name and url to be non optional.

Related

Siri Shortcuts with CoreData

I created a simple SwiftUI app with Core Data and I want to be able to add data via the shortcuts app, I created a shortcut that takes some text as input and returns it in uppercase and when I run the shortcut in the shortcuts app, it works, however when I added an "add" function (to save data in the Core Data database) to the intent handle function, and I run it again nothing is saved in the app, here is the code:
class MakeUppercaseIntentHandler: NSObject, MakeUppercaseIntentHandling {
let persistenceController = PersistenceController()
func handle(intent: MakeUppercaseIntent, completion: #escaping (MakeUppercaseIntentResponse) -> Void) {
if let inputText = intent.text {
let uppercaseText = inputText.uppercased()
completion(MakeUppercaseIntentResponse.success(result: add(text: uppercaseText)))
} else {
completion(MakeUppercaseIntentResponse.failure(error: "The text entred is invalid"))
}
}
func resolveText(for intent: MakeUppercaseIntent, with completion: #escaping (MakeUppercaseTextResolutionResult) -> Void) {
if let text = intent.text, !text.isEmpty {
completion(MakeUppercaseTextResolutionResult.success(with: text))
} else {
completion(MakeUppercaseTextResolutionResult.unsupported(forReason: .noText))
}
}
func add(text: String) -> String{
let newItem = Item(context: persistenceController.container.viewContext)
newItem.text = text
do {
try persistenceController.container.viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return text
}
}
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "SiriShort")
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.SiriShortcut2")?.appendingPathComponent("SiriShort.sqlite") else {
fatalError("Shared file container could not be created.")
}
let storeDescription = NSPersistentStoreDescription(url: fileContainer)
storeDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
storeDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
}
}
Thank you

Migrate Core Data from Swift to SwiftUI

I am attempting to migrate an app from Swift to SwiftUI but am struggling with Core Data. I run both the Swift and SwiftUI apps under the same bundle identifier so they are accessing the same underlying data but although I use the same xcdatamodeld model name for both, they both point to different data bases.
What I need to do is run the app on Swift and load data into Core Data. Then re-run the SwiftUI version of the app and be able to load the identical data.
Here the code for the Swift version:
class DataStore {
static let sharedDataStore = DataStore()
var managedContext: NSManagedObjectContext!
lazy var coreDataStack = CoreDataStack()
fileprivate init() {
self.managedContext = coreDataStack.context
}
func createParcours() -> Parcours {
let parcours = Parcours(context: managedContext)
parcours.timeStamp = NSDate()
return parcours
}
func deleteParcours(_ toDelete: Parcours) {
managedContext.delete(toDelete)
self.saveParcours()
}
func saveContext(parcours: Parcours?) {
if let parcours = parcours {
encodeParcours(parcours)
}
coreDataStack.saveContext()
}
}
class CoreDataStack {
let modelName = "MyParcours" // Exactly same name as name.xcdatamodeld
fileprivate lazy var applicationDocumentsDirectory: URL = {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return urls[urls.count-1]
}()
lazy var context: NSManagedObjectContext = {
var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = self.psc
return managedObjectContext
}()
fileprivate lazy var psc: NSPersistentStoreCoordinator = {
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.appendingPathComponent(self.modelName)
do {
let options = [NSMigratePersistentStoresAutomaticallyOption: true]
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: options)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject?
dict[NSLocalizedFailureReasonErrorKey] = "There was an error creating or loading the application's saved data." as AnyObject?
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() 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.
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return coordinator
}()
fileprivate lazy var managedObjectModel: NSManagedObjectModel = {
let modelURL = Bundle.main.url(forResource: self.modelName, withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
func saveContext () {
guard context.hasChanges else {return}
do {
try context.save()
} catch let error as NSError {
print("Unresolved error: \(error), \(error.userInfo)")
}
}
}
And in the SwiftUI version, I generate the NSPersistentContainer() and inject it into the ContentView:
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "MyParcours")
init() {
container.loadPersistentStores { NSEntityDescription, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
#main
struct MySwiftUIApp: App {
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
Any pointers where I am going wrong?
I found out why the database did not show up in the SwiftUI version of the app. The reason is that Apple changed the storage location in some earlier version of iOS (not certain exactly when), originally located in the Documents folder and now in the Library/Application%20Support.
So the solution is to change the url of the NSPersistentStoreDescription:
init() {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0].appendingPathComponent("MyParcours")
self.container = NSPersistentContainer(name: "MyParcours")
// Change URL to allow for compatibility with older version in Swift
let description = NSPersistentStoreDescription(url: documentsDirectory)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { NSEntityDescription, error in
etc.

Migrating Data to App Groups Disables iCloud Syncing

I am adding a Today Extension to my existing app. I have added the an App Group and used this post to successfully migrate my Core Data's data to the App Group's store. My app uses both a NSPersistentCloudKitContainer (when iCloud is toggled on) and a NSPersistentContainer (iCloud toggled off). While the data in both containers migrate successfully, I am no longer able to sync between my devices when using NSPersistentCloudKitContainer. In the console I get these two errors:
CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(837): : Failed to set up CloudKit integration for store: (URL: file://path/name.sqlite)
Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}
The path from the first error message is the path of the oldURL before switching to App Groups. So I believe I just need to tell iCloud to not try to integrate CloudKit at that store location and use the App Group's store location.
But I can not figure out how to do this. Can anyone help?
Core Data code:
class CoreDataManager {
static let sharedManager = CoreDataManager()
private init() {}
lazy var persistentContainer: NSPersistentContainer = {
var useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")
//Get the correct container
let containerToUse: NSPersistentContainer?
if useCloudSync {
containerToUse = NSPersistentCloudKitContainer(name: "App")
} else {
containerToUse = NSPersistentContainer(name: "App")
}
guard let container = containerToUse else {
fatalError("Couldn't get a container")
}
//Set the storeDescription
let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.App")!.appendingPathComponent("\(container.name).sqlite")
var defaultURL: URL?
if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
}
if defaultURL == nil {
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
}
let description = container.persistentStoreDescriptions.first else {
fatalError("Hey Listen! ###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if !useCloudSync {
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
//migrate from old url to use app groups
if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
let coordinator = container.persistentStoreCoordinator
if let oldStore = coordinator.persistentStore(for: url) {
do {
try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
} catch {
print("Hey Listen! Error migrating persistent store")
print(error.localizedDescription)
}
// delete old store
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: { url in
do {
try FileManager.default.removeItem(at: url)
} catch {
print("Hey Listen! Error deleting old persistent store")
print(error.localizedDescription)
}
})
}
}
}
return container
}
}
If you are still facing the same problem, you should add the following line for the storeDescription
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.yourapp.identifier")
Source: https://developer.apple.com/videos/play/wwdc2019/202/
Following is my CoreDataStack:
import CoreData
class CoreDataStack {
// MARK: - Core Data stack
static var persistentContainer: NSPersistentCloudKitContainer = {
/*
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 = NSPersistentCloudKitContainer(name: "data")
let storeURL = URL.storeURL(for: "group.com.myapp", databaseName: "data")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.myapp")
container.persistentStoreDescriptions = [storeDescription]
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)")
}
})
return container
}()
// MARK: - Core Data Saving support
static func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// 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.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
public extension URL {
/// Returns a URL for the given app group and database pointing to the sqlite database.
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}

How to save a string array to core data in swift

I'm trying to save a string array to core data, but every time I try to read the array a get an empty one(I'm using NSString because of an answer I've read here, but it doesn't seem to work). Here is the code I use for saving :
let i = UserDefaults.standard.integer(forKey: "Index")
let fetchRequest: NSFetchRequest<PersonalTask> = PersonalTask.fetchRequest()
do {
let tasks = try PersistanceService.context.fetch(fetchRequest)
self.personalTasks = tasks
} catch {
print("Error")
}
let pTask = personalTasks[i]
var subtasks = pTask.subtasks
print(subtasks as Any)
let subtask = textView!.text as NSString
subtasks?.append(subtask)
print(subtasks)
PersistanceService.saveContext()
Here is the function saveContext() :
static var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
static var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "Tasks")
guard let description = container.persistentStoreDescriptions.first else {
fatalError("No descriptions found")
}
description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
// MARK: - Core Data Saving support
static func saveContext () {
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
print("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
And here I am reading the data and getting an empty array :
let i = UserDefaults.standard.integer(forKey: "Index")
let fetchRequest: NSFetchRequest<PersonalTask> = PersonalTask.fetchRequest()
do {
let tasks = try PersistanceService.context.fetch(fetchRequest)
self.personalTasks = tasks
} catch {
print("Error")
}
let pTask = personalTasks[i]
print(pTask.title)
print(pTask.notes)
print(pTask.subtasks)
subtasks = pTask.subtasks as [String]? ?? [NSString]() as [String]
print(subtasks)
What should I do to fix this?

Proper singleton class to use CoreData

I'm trying to create a singleton class which works with an NSManagedObjectContext.
This is the class:
import Foundation
import CoreData
class PersistenceService{
init(){}
// MARK: - Core Data stack
static var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
static var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "frazeit")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
static func saveContext () {
let mainContext = persistentContainer.viewContext
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = mainContext
privateContext.perform {
if privateContext.hasChanges {
do {
try privateContext.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
}
In some occasion, it does not push changes into the persistent store, while the app is open the persistent container is changed but when I re-run the app changes are gone. What's the right way to save the changes into the persistent store.
This the class that does not work properly:
class func add(word: String, quotes:[Quotes], language: String){
for item in quotes {
if let phrase = item.phrase, let author = item.author {
let quote = CachedQuotes(context: PersistenceService.context)
quote.phrase = phrase
quote.date = Date() as NSDate
quote.keyword = word
quote.language = language
quote.author = author
PersistenceService.saveContext()
}
}
}
I call it to save quotes which are fetched from the network:
override func viewDidLoad() {
let quotes = CachedQuotes.getAllQuotes()
//Prints the number of saved records which is 0 now
self.getQuote { (result, error) in
if let qoutes = result?.quotes {
CachedQuotes.add(word: "friend", quotes: qoutes, language: "en")
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let quotes = CachedQuotes.getAllQuotes()
//Prints the number of saved records which is 10 now
}
But when I re-run the app, nothing is saved into the persistance container.
UPDATE:
The code below works now
static func saveContext () {
let mainContext = persistentContainer.viewContext
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.automaticallyMergesChangesFromParent = true
privateContext.parent = mainContext
privateContext.perform {
do {
try privateContext.save()
mainContext.perform({
do {
try mainContext.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
})
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
First it saves the private quoue then saves the main.
let mainContext = persistentContainer.viewContext
let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateContext.parent = mainContext
You edit a context and then save the same context to persist the changes. Creating a child context to .viewContext and saving said child context does not save the .viewContext itself, where you made changes.
If you want to use background queues, first set var automaticallyMergesChangesFromParent: Bool on the .viewContext where you want to receive changes from the background queue. Then you create a background context, set on it the same persistentStoreCoordinator from .viewContext, make changes on it and then save the background queue.
Using privateContext.perform is a good start. You can do better if you wrap the changes to quote in a perform through the context in which the quote was created in the first place, so you access quote through the same thread the context uses.
Here is the singleton from Apple's Refreshing and Maintaining Your App Using Background Tasks sample.
import Foundation
import CoreData
class PersistentContainer: NSPersistentContainer {
private static let lastCleanedKey = "lastCleaned"
static let shared: PersistentContainer = {
ValueTransformer.setValueTransformer(ColorTransformer(), forName: NSValueTransformerName(rawValue: String(describing: ColorTransformer.self)))
let container = PersistentContainer(name: "ColorFeed")
container.loadPersistentStores { (desc, error) in
if let error = error {
fatalError("Unresolved error \(error)")
}
print("Successfully loaded persistent store at: \(desc.url?.description ?? "nil")")
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
return container
}()
var lastCleaned: Date? {
get {
return UserDefaults.standard.object(forKey: PersistentContainer.lastCleanedKey) as? Date
}
set {
UserDefaults.standard.set(newValue, forKey: PersistentContainer.lastCleanedKey)
}
}
override func newBackgroundContext() -> NSManagedObjectContext {
let backgroundContext = super.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyStoreTrumpMergePolicyType)
return backgroundContext
}
}
Personally I prefer passing the NSPersistentContainer around via dependency injection but it requires a lot more effort.