How to set a custom store URL for NSPersistentContainer - core-data

How can I set a custom store.sqlite URL to NSPersistentContainer?
I have found an ugly way, subclassing NSPersistentContainer:
final public class PersistentContainer: NSPersistentContainer {
private static var customUrl: URL?
public init(name: String, managedObjectModel model: NSManagedObjectModel, customStoreDirectory baseUrl:URL?) {
super.init(name: name, managedObjectModel: model)
PersistentContainer.customUrl = baseUrl
}
override public class func defaultDirectoryURL() -> URL {
return (customUrl != nil) ? customUrl! : super.defaultDirectoryURL()
}
}
Is there a nice way?
Background: I need to save to an App Groups shared directory.

You do this with the NSPersistentStoreDescription class. It has an initializer which you can use to provide a file URL where the persistent store file should go.
let description = NSPersistentStoreDescription(url: myURL)
Then, use NSPersistentContainer's persistentStoreDescriptions attribute to tell it to use this custom location.
container.persistentStoreDescriptions = [description]
Note: myURL must provide the complete /path/to/model.sqlite, even if it does not exist yet. It will not work to set the parent directory only.

Expanding on Tom's answer, when you use NSPersistentStoreDescription for any purpose, be sure to init with NSPersistentStoreDescription(url:) because in my experience if you use the basic initializer NSPersistentStoreDescription() and loadPersistentStores() based on that description, it will overwrite the existing persistent store and all its data the next time you build and run. Here's the code I use for setting the URL and description:
let container = NSPersistentContainer(name: "MyApp")
let storeDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
// or
let storeDirectory = NSPersistentContainer.defaultDirectoryURL()
let url = storeDirectory.appendingPathComponent("MyApp.sqlite")
let description = NSPersistentStoreDescription(url: url)
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { (storeDescription, error) in
if let error = error as? NSError {
print("Unresolved error: \(error), \(error.userInfo)")
}
}

I just find out that the location of db created by PersistentContainer is different from db created by UIManagedDocument. Here is a snapshot of db location by UIManagedDocument:
and the following codes are used to create the db:
let fileURL = db.fileURL // url to ".../Documents/defaultDatabase"
let fileExist = FileManager.default.fileExists(atPath: fileURL.path)
if fileExist {
let state = db.documentState
if state.contains(UIDocumentState.closed) {
db.open()
}
} else {
// Create database
db.save(to: fileURL, for:.forCreating)
}
It looks like that the db referred by PersistentContainer is actually the file further down under folder "StoreContent" as "persistentStore"
This may explain why the db "defaultDatabase" in my case cannot be created by PersistentContainer if you want to specify your customized db file, or causing crash since folder already existed. I further verified this by appending a file name "MyDb.sqlite" like this:
let url = db.fileURL.appendingPathComponent("MyDb.sqlite")
let storeDesription = NSPersistentStoreDescription(url: url)
container.persistentStoreDescriptions = [storeDesription]
print("store description \(container.persistentStoreDescriptions)"
// store description [<NSPersistentStoreDescription: 0x60000005cc50> (type: SQLite, url: file:///Users/.../Documents/defaultDatabase/MyDb.sqlite)
container.loadPersistentStores() { ... }
Here is the new MyDb.sqlite:
Based on the above analysis, if you have codes like this:
if #available(iOS 10.0, *) {
// load db by using PersistentContainer
...
} else {
// Fallback on UIManagedDocument method to load db
...
}
Users' device may be on iOS pre 10.0 and later be updated to 10+. For this change, I think that the url has to be adjusted to avoid either crash or creating a new(empty) db (losing data).

This is the code that I use to initialize a pre-populated sqlite db that works consistently. Assuming you will use this db as read only then there is no need to copy it to the Documents dir on the device.
let repoName = "MyPrepopulatedDB"
let container = NSPersistentContainer(name: repoName)
let urlStr = Bundle.main.path(forResource: "MyPrepopulatedDB", ofType: "sqlite")
let url = URL(fileURLWithPath: urlStr!)
let persistentStoreDescription = NSPersistentStoreDescription(url: url)
persistentStoreDescription.setOption(NSString("true"), forKey: NSReadOnlyPersistentStoreOption)
container.persistentStoreDescriptions = [persistentStoreDescription]
container.loadPersistentStores(completionHandler: { description, error in
if let error = error {
os_log("ERROR: Failed to initialize persistent store, error is \(error.localizedDescription)")
} else {
os_log("Successfully loaded persistent store, \(description)")
}
})
Some very important steps/items to keep in mind:
when constructing the URL to the sqlite file use the URL(fileURLWithPath:) form of the initializer. It seems that core data requires file based URLs, otherwise you will get an error.
I used a unit test to run some code in order to create/pre-populate the db in the simulator.
I located the full path to the sqlite file by adding a print statement inside the completion block of loadPersistentStores(). The description parameter of this block contains the full path to the sqlite file.
Then using Finder you can copy/paste that file in the app project.
At the same location as the sqlite file there are two other files (.sqlite-shm & .sqlite-wal). Add these two to the project also (in the same directory as the sqlite file). Without them core data throws an error.
Set the NSReadOnlyPersistentStoreOption in persistentStoreDescription (as shown above). Without this you get a warning (possible future fatal error).

Related

Document Controller search handling of non file: URLs

Global documents with a custom URL scheme?
I have a need to cache info via a URL, with a custom scheme - non file:; to allow user access, and otherwise treat such URLs as global so any access via its URL sees the same data. It's just a fancy way to access user defaults.
I'm relying on a document controller's document(url:) to find such URL if its document exits - previously opened.
And yet it doesn't?
Consider this in app's did finish launching:
do {
let ibm = URL.init(string: "https://www.ibm.com")!
let doc = try docController.makeDocument(withContentsOf: ibm, ofType: "myType")
assert((doc == docController.document(for: ibm)), "created document is not found?")
} catch let error {
NSApp.presentError(error)
}
The assert fires!
So I pause and try to figure what I'm doing wrong.
Essentially I'm trying to support non-file: info, in a flat namespace, to provide consistent access and content.
Probably not an answer - why such URL schemes aren't being found but a working solution is to cache anything, front the search method with such a cache, but doing so creates a maintenance issue:
#objc dynamic var docCache = [URL:NSDocument]()
override var documents: [NSDocument] {
let appDocuments = Array(Set([Array(docCache.values),super.documents].reduce([], +)))
return appDocuments
}
override func document(for url: URL) -> NSDocument? {
if let document = super.document(for: url) {
docCache[url] = document
return document
}
else
if let document = docCache[url] {
return document
}
else
{
return nil
}
}
Enjoy.

How to store the uri of image from registerForActivityResult() and send it to another function

The image i've selected from gallery disappear after the configuration changes, so i wanted to get the uri of the image so i can pass it to onSaveInstanceState but i don't know how to do so.
val selectImageFromGalleryResult = registerForActivityResult(
ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let {
profileImage.setImageURI(uri)
}
}
selectBtn.setOnClickListener {
profileImage.visibility = View.VISIBLE
selectImageFromGalleryResult.launch("image/*")
}
If i create a global variable,
private lateinit var imageUri: Uri
i get error saying it has not been initialized if i get the value from the above function and try to send it to onSaveInstanceState
Uri is Parcelable itself, you can directly put it into the Bundle. To help with saving it to state, you may need to create member variable in your activity
private var profileImageUri: Uri? = null
and once you obtain Uri from ActivityResultContracts.GetContent(), then store it in profileImageUri
val selectImageFromGalleryResult = registerForActivityResult(
ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let {
profileImageUri = uri
profileImage.setImageURI(uri)
}
}
and save this profileImageUri in Bundle at onSaveInstanceState()
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
outState.putParcelable("profileImageUri", profileImageUri)
super.onSaveInstanceState(outState, outPersistentState)
}
and to make sure on configuration change the image view load the uri, put below code in your onCreate() function
profileImageUri = savedInstanceState?.getParcelable("profileImageUri")
profileImageUri?.let {
profileImage.setImageURI(it)
}
Additional: if you use ViewModel, it will give you more concise way. Just can create an object to save the Uri in the ViewModel and no longer need to create boilerplate code above to survive the Uri on configuration changes

Invoking Xtext Generator via Language Server Protocol explicitly

I have a DSL project using Xtext together with the Language Server Protocol.
Now I want to trigger a Generator from the client (in my case VS Code) to be executed on the server.
Not automatically (I would know how to do that), but explicitly triggered by the user from a VS Code command.
I am aware of the IExecutableCommandService in Xtext and I know how to hook into it.
But I don't know how to retrieve the corresponding Resource from a given file path:
#Override
public Object execute(ExecuteCommandParams params, ILanguageServerAccess access, CancelIndicator cancelIndicator) {
if ("c4.generator.type".equals(params.getCommand())) {
// fileURI passed from client to the server
String fileURI = ((JsonPrimitive)params.getArguments().get(0)).getAsString();
// This is where I stuck
Resource resource = whatAPItoCallToRetrieveResourceFromFile(fileURI);
// Call the generator then
doGenerate(resource);
}
return "Unknown Command";
}
The use-case is the same as described in this blog: https://dietrich-it.de/xtext/2011/10/15/xtext-calling-the-generator-from-a-context-menu/
But the description is for Eclipse only (not using lsp).
If you already have the correct URI, you should be able to use XtextResourceSet to get access to the resource:
final XtextResourceSet rs = new XtextResourceSet();
final Resource r = rs.getResource(fileURI, true);
doGenerate(r);
otherwise you can get access to the Xtext index and iterate over all resources searching for the resource of interest, by using access.doReadIndex:
final XtextResourceSet rs = new XtextResourceSet();
final Function<ILanguageServerAccess.IndexContext, Boolean> func = (
ILanguageServerAccess.IndexContext ctxt) -> {
for (final IResourceDescription rd: ctxt.getIndex().getAllResourceDescriptions()) {
if(<check_rd>) {
Resource r = rs.getResource(rd.getURI(), true);
doGenerate(r);
}
}
return true;
};
access.doReadIndex(func);

Exporting Core Data structure to .csv file attached to mail XCODE 8.1 Swift 3

I'm making an app that needs to take a managed object array from core data and export it to a csv file that I plan to attach to an email being sent out using the mfMailComposer system. I have the data properly stored in the core data systems and the mail composer functionality seems to be working. I'm reaching a snag when I try to find the correct process by which to export the data.
I have already taken a long look at both of these posts attempting to determine a solution:
from 2012, seems very outdated:
how to export Core Data to CSV
from 2016, more recent, but swift 3 and Xcode 8 have since been released and I worry this has become outdated as well: How to create a CSV file from Core Data (swift)
I have been attempting to try the solutions proposed in the second link, but much of the code gets marked as incorrect when typing it, so I believe it is now obsolete with the upgrade.
The code below is based off of the second post and therefore, likely outdated, but in order to provide a reference of the process I am trying to accomplish...
// Called by the press of xcode UI button
#IBAction func ExportToCSV(_ sender: AnyObject)
{
// Make our mail composer controller and fill it with the proper information
let mailComposeViewController = configuredMailComposeViewController()
// If the composer is functioning properly ...
if MFMailComposeViewController.canSendMail()
{
// ... Present the generated mail composer controller
self.present(mailComposeViewController, animated: true, completion: nil)
}
else
{
// ... Otherwise, show why it is not working properly
self.showSendMailErrorAlert()
}
}
// Used to set up the body of the outgoing email
func configuredMailComposeViewController() -> MFMailComposeViewController
{
// Establish the controller from scratch
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self
// Set preset information included in the email
mailComposerVC.setSubject("Generic email subject")
mailComposerVC.setMessageBody("Generic email body", isHTML: false)
// Turn core data for responses into a .csv file
// Pull core data in
var CoreDataResultsList = [NSManagedObject]()
// Register the proper delegate and managed context
let appDelegate =
UIApplication.shared.delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
// Pull the data from core data
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "ItemResponses")
do {
let results =
try managedContext!.fetch(fetchRequest)
CoreDataResultsList = results as! [NSManagedObject]
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
// Take the managed object array and turn it into a .csv sring to write in the file
let csvString = writeCoreObjectsToCSV(objects: CoreDataResultsList, named: "Generic name")
let data = csvString.dataUsingEncoding(NSUTF8StringEncoding)
mailComposerVC.addAttachmentData(data, mimeType: "text/csv", fileName: "GenericFilename.csv")
return mailComposerVC
}
// Takes a managed object and writes it to the .csv file ..?
func writeCoreObjectsToCSV(objects: [NSManagedObject], named: String) -> String
{
// Make sure we have some data to export
guard objects.count > 0 else
{
return ""
}
let firstObject = objects[0]
let attribs = Array(firstObject.entity.attributesByName.keys)
// The attires.reduce function is throwing an error about originally using combine as in the second post, used auto fix, but noteworthy.
//Now gives an error that says "No '+' candidates produce the expected contextual result type NSString"
let csvHeaderString = (attribs.reduce("", {($0 as String) + "," + $1 }) as NSString).substringFromIndex(1) + "\n"
// This function says that substring from index has been renamed as well as a few other lines within it
let csvArray = objects.map({object in
(attribs.map({((object.valueForKey($0) ?? "NIL") as AnyObject).description}).reduce("",combine: {$0 + "," + $1}) as NSString).substringFromIndex(1) + "\n"
})
// Again with the reduce issue
let csvString = csvArray.reduce("", combine: +)
return csvHeaderString + csvString
}
New the bottom of the code I have commented in the multiple errors with the suggested code from the second post and the issues pertaining after I use xCode's auto-fix feature.
I would like to thank you in advance for helping me with this issue. I am merely looking for the most up-to-date way to export core data as a .csv file and send it out. Thanks!
I ended up working around it. At first I didn't understand how .csv files were written until I saw the acronym stood for "comma-separated values". Then it all clicked for me and I wrote my own, I did a more manual route for the head entry but the data on the following lines is still auto mated.
Below is the relevant functions in their new working form:
// Called by the press of xcode UI button
#IBAction func ExportToCSV(_ sender: AnyObject)
{
// Make our mail composer controller and fill it with the proper information
let mailComposeViewController = configuredMailComposeViewController()
// If the composer is functioning properly ...
if MFMailComposeViewController.canSendMail()
{
// ... Present the generated mail composer controller
self.present(mailComposeViewController, animated: true, completion: nil)
}
else
{
// ... Otherwise, show why it is not working properly
self.showSendMailErrorAlert()
}
}
// Used to set up the body of the outgoing email
func configuredMailComposeViewController() -> MFMailComposeViewController
{
// Establish the controller from scratch
let mailComposerVC = MFMailComposeViewController()
mailComposerVC.mailComposeDelegate = self
// Set preset information included in the email
mailComposerVC.setSubject("Generic Subject")
mailComposerVC.setMessageBody("Generic Email Body", isHTML: false)
// Turn core data for responses into a .csv file
// Pull core data in
var CoreDataResultsList = [NSManagedObject]()
// Register the proper delegate and managed context
let appDelegate =
UIApplication.shared.delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
// Pull the data from core data
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "ItemResponses")
do {
let results =
try managedContext!.fetch(fetchRequest)
CoreDataResultsList = results as! [NSManagedObject]
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
// Take the managed object array and turn it into a .csv sring to write in the file
// In doing this, we are writing just like we would to any string
let csvString = writeCoreObjectsToCSV(objects: CoreDataResultsList)
let data = csvString.data(using: String.Encoding.utf8.rawValue, allowLossyConversion: false)
mailComposerVC.addAttachmentData(data!, mimeType: "text/csv", fileName: "GenericFilename.csv")
return mailComposerVC
}
// Takes a managed object and writes it to the .csv file ..?
func writeCoreObjectsToCSV(objects: [NSManagedObject]) -> NSMutableString
{
// Make sure we have some data to export
guard objects.count > 0 else
{
return ""
}
var mailString = NSMutableString()
mailString.append("Generic Header 1, Generic Header 2, Generic Header 3")
for object in objects
{
// Put "\n" at the beginning so you don't have an extra row at the end
mailString.append("\n\(object.value(forKey: "Generic Core Data Key 1")!),\(object.value(forKey: "Generic Core Data Key 2")!), \(object.value(forKey: "Generic Core Data Key 3")!)")
}
return mailString
}
I'm having an issue where one of my keys is a string containing commas and need a proper way to escape from them. I've heard double quotes is how to do it but inserting them gave me no success.
Regardless, this is one current way to take core data into an array and write it to a string, save it to a .csv file and mail it out. This answers my question, but my next task is reading the data back in. I have no idea how to access the file to do that on an iPad. If you come across this and know how to do that, please let me know! I will probably be making another post on that topic if I can't find a solution and will then drop a new question and a link to that in the replies below this answer.
Thank you!

Swift 3 Fetch Request Error (warning: could not load any Objective-C class information)

I recently received this error when fetching data from Core Data:
warning: could not load any Objective-C class information. This will significantly reduce the quality of type information available.
(lldb)
Here is my code:
// MARK: - Initialize Fetch Request
var fetchedResultsController = NSFetchedResultsController<Profile>()
func setFetchRequest() -> NSFetchRequest<Profile> {
let request = Profile.fetchRequest()
let sortDescriptor = SortDescriptor(key: "title", ascending: false)
do {
try moc?.fetch(request)
} catch {
print("Error With Request: \(error)")
}
request.sortDescriptors = [sortDescriptor]
return setFetchRequest()
}
// MARK: - Retrieve Fetch Request
func getFetchRequest() -> NSFetchedResultsController<Profile> {
fetchedResultsController = NSFetchedResultsController(fetchRequest: setFetchRequest(), managedObjectContext: moc!, sectionNameKeyPath: nil, cacheName: nil)
return fetchedResultsController
}
I crashed with this error where I have "try moc?.fetch(request)":
Thread 1 EXC_BAD_ACCESS (code=2, address=0x16fc07feo)
Are these errors connected or is this a bug in Swift 3 / Xcode 8?
You shouldn't take results from the ManagedObjectContext. If you want to use a NSFetchedResultsController class in your app? You'll need to access their methods. And all of the required or optional methods are comes from the NSFetchedResultsControllerDelegate protocol.
Try this
class YourTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
var fetchedResultsController:NSFetchedResultsController<Profile>!
}
And then create a custom helper function like this one:
`func frc() {
let request:NSFetchRequest<Profile> = Profile.fetchRequest()
let sorter = SortDescriptor(key: "title", ascending: true)
request.sortDescriptors = [sorter]
self.fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
// make sure the delegate is set to self
self.fetchedResultsController.delegate = self
do {
try self.fetchedResultsController.performFetch()
} catch {}
}
`
From this point you'll need a trigger to perform operations. So let's the system itself should be doing this when you call the viewDidLoad method or you can create a button instead. For example click the button to begin operations.
override func viewDidLoad() {
super.viewDidLoad()
self.frc()
self.tableView.reloadData()
}
It should be works.
Good luck!
Automatic Subclass Generation
Xcode 8 and Swift 3 comes with a new generation of subclassing called as Automatic Subclass Generation! How to create it? Well! So let's create a new Xcode 8 project, choose a Single View Application and then another window will appears called Choose options for your new project:. Give the name for your new project, make sure language is a Swift and Use Core Data check box is checked and then hit Create.
Go to the YourProjectName.xcdatamodeld file and click it. And then add an entity! So let's say your entity name is a Profile and create their Attributes respectively. It's important to know, because this is an Automatic Subclass Generation. Choose your entity and go to the Data Model Inspector ! Choose a Class Definition for the Codegen You can find a Codegen from here.
After selected the Class Definition, you can see Name text field automatically filled by your entity name like so. Again go to the your entity and click it. Click Command + S for save changes firstly and then click Command + B for rebuild, that's it. Automatic Subclass Generation is successfully created.
Remember
If you want to change your model? For example: If you want to add a new Attribute to your model? It's very easy, select a xcdatamodeld file and click your entity. Click the plus sign under Attributes and add your new Attribute. After your changes is completed? Don't forget to save changes. Again click Command + S and then click Command + B
Create A Managed Object
In the Swift 3 you can create a ManagedObject by using subclass initializer. It's very easy to implementing than ever before
let managedObject = Profile(context: self.managedObjectContext!)
You can see it's very easy! How to save values to the managedObject ? So let's say you have a title attribute of your model. Also title is a String.
managedObject.setValue("Well crafted API? Right?", forKey: "title")
or
managedObject.title = "Well crafted API? Right?"
Save values:
do {
try self.managedObjectContext.save()
print(managedObject)
} catch {}
It's works well on the Swift 3.0 beta and Xcode 8.0 beta.
Update
So, this is what I got working for Xcode 8 beta and Swift 3 beta Core Data
var fetchedResultsControler = NSFetchedResultsController<Profile>()
func frc() {
let request: NSFetchRequest<Profile> = Profile.fetchRequest()
let sortDescriptor = SortDescriptor(key: "title", ascending: true)
request.sortDescriptors = [sortDescriptor]
self.fetchedResultsControler = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.moc!, sectionNameKeyPath: nil, cacheName: nil)
self.fetchedResultsControler.delegate = self
do {
try self.fetchedResultsControler.performFetch()
} catch {
print("Error Fetching Data: \(error)")
}
}
and in viewDidLoad I have self.frc() at the top of the viewDidLoad.
So, in my Profile+CoreDataProperties.swift I copied a method Apple uses in their Master-Detail example when you create a new project:
extension Profile {
#nonobjc class func fetchRequest() -> NSFetchRequest<Profile> {
return NSFetchRequest<Profile>(entityName: "Profile");
}
#NSManaged var title: String?
#NSManaged var titleImage: Data
}
so that my fetch request is "native to my function." Pretty sure that's not the correct way to say that but it's helping me understand what is going on. The fetch request in Apple's example is green instead of blue. And it took me forever to notice that. I clicked on "Event" in Apple's example, and was conveniently taken to the created subclass, which was demonstrated in the Xcode 8 video at WWDC.
The files for e.g. Event.swift and Event+CoreDataProperties.swift are not exposed like they are in Xcode 7.x.x and earlier. You have to click on the entity in the code and you'll be taken to them. Maybe that was my problem? Anyway, I'm fetching data and images like a champ. Thanks a lot for your help #Mannopson!

Resources