NSDocument based application to read/write sqlite db - cocoa

I'm implementing an application for Mac OS X to easily access to SQLite databases. Since the user will be able to work on more files at the same time, I opted for a document based application.
Most books and tutorials explain how to create and read data in such a context from a file using NSData. My app, instead, should rely on the SQLite library for that purpose.
What are the methods I should override in order to do this?

Only -[NSDocument readFromURL:ofType:error:]. I suppose every change is saved automatically (SQLite), so you don't need a save action.
I have also done this and I liked it pretty much.
For example:
- (BOOL)readFromURL:(NSURL *)URL ofType:(NSString *)type error:(NSError **)error {
if (sqlite3_open_v2([[URL path] UTF8String], &db, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK) {
sqlite3_close(db);
return NO;
}
return YES;
}

Related

Xcode MagicalRecord avoid WAL files generation

I'm building my first App with Magical Record and I'm wondering if there is a way to avoid the generation of the three files (dbname, dbname-shm, dbname-wal) for my model and proceed with the generation of a single file (dbname.sqlite).
I don't know where to set this string in MR files:
#{NSSQLitePragmasOption: #{#"journal_mode": #"delete"}}
if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:#{NSSQLitePragmasOption: #{#"journal_mode": #"delete"}, NSMigratePersistentStoresAutomaticallyOption:#YES, NSInferMappingModelAutomaticallyOption:#YES} error:&error]) {
[[NSApplication sharedApplication] presentError:error];
return nil;
}
I looked into NSPersistentStoreCoordinator+MagicalRecord.m with no results.
If all else fails, you can do everything the plain old core data way and use the default add persistent store method on the persistent store coordinator.

Updating/replacing CoreData sqlite file in app

My app currently have this sqlite file (lets name it v1)
What i want to do is that when i activate a IBAction , it will automatically delete the current file (v1) and retrieve values from webservice and store inside a new file (v2) and the application will then use the new sqlite file (v2)
any idea on how to implement?
my current code is here:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}
NSURL *storeUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: #"test.sqlite"]];
NSLog(#"%#",storeUrl);
NSError *error = nil;
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return persistentStoreCoordinator;
}
You can:
download the new database from the server
reset the whole Core Data context (store coordinator, managed object contexts, managed objects)
replace the old database with the new one
ask the UI to refresh to display the newest content: the Core Data context should reload lazily
But this approach is quite dangerous, as you'd have to make sure every Core Data object in your app can be reset. Also, replacing the database should happen just after resetting the context, to avoid a race condition. If you have threads / blocks accessing Core Data this is even more difficult.
A better approach may be to update the database records by downloading a JSON file that contains the new database's contents, delete the old records, and insert the new ones. This also makes sure that you can update your Core Data schema in a future version of the app without breaking your content updating process. And by using the NSFetchResultsController class, your table views or other UI elements may even update automatically with a nice animation as your database gets updated.
If you care about bandwidth, I use protocol buffers which are way more compact than any JSON or even sqlite file would be, and quite easy to use.

Why is -[NSPasteboard writeObjects:] returning NO for an array of NSURL objects?

I have an NSArrayController that I'm using to provide data to an IKImageBrowserView. I want to support drag and drop from the IKImageBrowserView to other applications. Here's the relevant method from my code:
- (NSUInteger) imageBrowser:(IKImageBrowserView *) aBrowser writeItemsAtIndexes:(NSIndexSet *) itemIndexes toPasteboard:(NSPasteboard *)pasteboard{
NSArray *items = [[resultsArrayController arrangedObjects] objectsAtIndexes:itemIndexes];
if(![pasteboard writeObjects:items]){
return 0;
}
return [items count];
}
My app is new so I'm targeting 10.6+ and according to the documentation, "On Mac OS X v10.6 and later, use writeObjects: to write URLs directly to the pasteboard instead."
I've verified that the objects that I am attempting to write are indeed NSURL objects, so I'm not sure where the process is breaking down or how to further troubleshoot the problem. Thanks in advance for any help.
Have you cleared the pasteboard yet? You need to do that, and thereby become the pasteboard's owner, before you can write objects to the pasteboard.
Have you verified that your pasteboard variable is not nil?
Are the objects that are to be sent to -[NSPasteboard writeObjects:] supposed to be NSPasteboadItems? You can set their string value to the absoluteString of the NSURL and write an array of NSPasteboadItems.

How do I share a Core Data store between processes using NSDistributedNotifications?

Background
I've already posted a question about the basics of sharing a Core Data store between processes.
I'm trying to implement the recommendations given and I'm running into problems.
My Goal
I have two processes - the Helper App and the UI. They both share a single data store. I want the UI to update it's NSManagedObjectContext when the Helper App has saved new data to the store.
Current Program Flow
The Helper App Process writes data to the Store.
In the Helper App, I listen for NSManagedObjectContextDidSaveNotification notifications.
When the context is saved, I encode the inserted, deleted and updated objects using their URI representations and NSArchiver.
I send an NSNotification to the NSDistributedNotificationCenter with this encoded dictionary as the userInfo.
The UI Process is listening for the save notification. When it receives the notification, it unarchives the userInfo using NSUnarchiver.
It looks up all the updated/inserted/deleted objects from the URIs given and replaces them with NSManagedObjects.
It constructs an NSNotification with the updated/inserted/deleted objects.
I call mergeChangesFromContextDidSaveNotification: on the Managed Object Context of the UI Process, passing in the NSNotification I constructed in the previous step.
The Problem
Inserted objects are faulted into the UI Managed Object Context fine and they appear in the UI. The problem comes with updated objects. They just don't update.
What I've tried
The most obvious thing to try would
be to pass the save Notification
from the Helper App process to the
UI process. Easy, right? Well, no.
Distributed Notifications won't
allow me to do that as the userInfo
dictionary is not in the right
format. That's why I'm doing all the
NSArchiving stuff.
I've tried calling
refreshObject:mergeChanges:YES on
the NSManagedObjects to be updated,
but this doesn't seem to have any
effect.
I've tried performing the
mergeChangesFromContextDidSaveNotification:
selector on the main thread and the
current thread. Neither seems to
affect the result.
I've tried using
mergeChangesFromContextDidSaveNotification:
before between threads, which of
course is much simpler and it worked
perfectly. But I need this same
functionality between processes.
Alternatives?
Am I missing something here? I'm consistently getting the feeling I'm making this much more complex than it needs to be, but after reading the documentation several times and spending a few solid days on this, I can't see any other way of refreshing the MOC of the UI.
Is there a more elegant way of doing this? Or am I just making a silly mistake somewhere in my code?
The Code
I've tried to make it as readable as possible, but it's still a mess. Sorry.
Helper App Code
-(void)workerThreadObjectContextDidSave:(NSNotification *)saveNotification {
NSMutableDictionary *savedObjectsEncodedURIs = [NSMutableDictionary dictionary];
NSArray *savedObjectKeys = [[saveNotification userInfo] allKeys];
for(NSString *thisSavedObjectKey in savedObjectKeys) {
// This is the set of updated/inserted/deleted NSManagedObjects.
NSSet *thisSavedObjectSet = [[saveNotification userInfo] objectForKey:thisSavedObjectKey];
NSMutableSet *thisSavedObjectSetEncoded = [NSMutableSet set];
for(id thisSavedObject in [thisSavedObjectSet allObjects]) {
// Construct a set of URIs that will be encoded as NSData
NSURL *thisSavedObjectURI = [[(NSManagedObject *)thisSavedObject objectID] URIRepresentation];
[thisSavedObjectSetEncoded addObject:thisSavedObjectURI];
}
// Archive the set of URIs.
[savedObjectsEncodedURIs setObject:[NSArchiver archivedDataWithRootObject:thisSavedObjectSetEncoded] forKey:thisSavedObjectKey];
}
if ([[savedObjectsEncodedURIs allValues] count] > 0) {
// Tell UI process there are new objects that need merging into it's MOC
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:#"com.synapticmishap.lapsus.save" object:#"HelperApp" userInfo:(NSDictionary *)savedObjectsEncodedURIs];
}
}
UI Code
-(void)mergeSavesIntoMOC:(NSNotification *)notification {
NSDictionary *objectsToRefresh = [notification userInfo];
NSMutableDictionary *notificationUserInfo = [NSMutableDictionary dictionary];
NSArray *savedObjectKeys = [[notification userInfo] allKeys];
for(NSString *thisSavedObjectKey in savedObjectKeys) {
// Iterate through all the URIs in the decoded set. For each URI, get the NSManagedObject and add it to a set.
NSSet *thisSavedObjectSetDecoded = [NSUnarchiver unarchiveObjectWithData:[[notification userInfo] objectForKey:thisSavedObjectKey]];
NSMutableSet *savedManagedObjectSet = [NSMutableSet set];
for(NSURL *thisSavedObjectURI in thisSavedObjectSetDecoded) {
NSManagedObject *thisSavedManagedObject = [managedObjectContext objectWithID:[persistentStoreCoordinator managedObjectIDForURIRepresentation:thisSavedObjectURI]];
[savedManagedObjectSet addObject:thisSavedManagedObject];
// If the object is to be updated, refresh the object and merge in changes.
// This doesn't work!
if ([thisSavedObjectKey isEqualToString:NSUpdatedObjectsKey]) {
[managedObjectContext refreshObject:thisSavedManagedObject mergeChanges:YES];
[managedObjectContext save:nil];
}
}
[notificationUserInfo setObject:savedManagedObjectSet forKey:thisSavedObjectKey];
}
// Build a notification suitable for merging changes into MOC.
NSNotification *saveNotification = [NSNotification notificationWithName:#"" object:nil userInfo:(NSDictionary *)notificationUserInfo];
[managedObjectContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:saveNotification
waitUntilDone:YES];
}
I used the method in
http://www.mlsite.net/blog/?p=518
then every object is correctly faulted but the faults are fetch in cache so still no update
I had to do
[moc stalenessInterval = 0];
And it finally worked, with relationship.
You're looking for - (void)refreshObject:(NSManagedObject *)object mergeChanges:(BOOL)flag I believe.
This will refresh the object with the info in the persistent store, merging changes if you want.
I'd go with Mike's suggestion and just watch the store file for changes.
Though it may not be the most efficient, I've had success using - [NSManagedObjectContext reset] from a second process when there's a change to a store. In my case case, the code is fairly linear — all I do is run a fetch request for some data after resetting. I don't know how this will work with bindings and a complicated UI, but you may be able to post a notification to manually update things if it's not handled automatically.
I had this exact same issue with an iPhone app that I've been working on. In my case, the solution involved setting the Context's stalenessInterval to something suitably infinitesimal (e.g., 0.5 seconds).
This works, except for sandboxes apps. You can't send a notification with a user info dict. Instead consider some other IPC like XPC or DO.
On a side note, using NSDustributedNotificationCenter is not always 100% if the system is busy.
Setting stalenessInterval of managed object context works. My case involves multiple threads instead of process though.
Starting with iOS 9, you should now use mergeChangesFromRemoteContextSave:intoContexts:. See this for an explanation: https://www.innoq.com/en/blog/ios-writing-core-data-in-today-extension/

Mounting a folder as a device in Finder using Cocoa

Is there a way to mount a folder on the hard disk as a device in Finder. The intend here is to provide the user with an easy way to get to a folder that my application uses to store data. I don't want my user to go searching for data in Application Data. I would rather allow them to make this data available as a mounted volume or device in Finder. I would also like this volume or device to be read/write, so that if the user makes any changes to the data files, the changes will get reflected in the original folder.
Is there a way to do this in cocoa, carbon or applescript.
Try looking into FUSE. You can have all sorts of psuedo filesystems with that.
But I'd caution a little against what you are trying to do. It may make more sense to just have a button that opens the folder in your application, rather than create a new device. I personally would find it hard to continue to use an application that does such a thing. It doesn't really fit with the rest of the available applications.
You could also use an alias to point to your Application Data directory.
You can use sparse disk image to create "fake" drive.
But why not make data directory configurable in your application? Or use subdirectory in ~/Documents/?
Alias/symlink on desktop will be the easiest solution:
ln -s '~/Application Data/Yourapp' '~/Desktop/Yourapp Data'
Can I suggest rethinking this entirely? A symlink or alias would work, but, if possible, a better idea would be to register for the filetypes people will be moving into that folder, and then respond to opening them by moving or copying them to the correct folder. I'm thinking of something like the Dashboard interface, where if you double-click a downloaded .wdgt file, it asks if you want to 'install' the widget and then, if you do, copies it into ~/Library/Widgets. Obviously, if you're dealing with common types like images, folders, or generic text files, this might be impractical.
For implementation, you'd just add the document types to your Info.plist, and handle them in you App Delegate's -application:openFile: method.
I would also urge caution with this, seems potentially somewhat confusing to most users. That said, have you considered simply creating a softlink to the directory in question?
I Do it using NSWorkspace. In my case I make an initial check with the function -(BOOL)isMountedPath;
The code for mounting is:
NSURL *path=[NSURL URLWithString:#"smb://server.resource/KEYS_DB"];
if(NO==[self isMountedPath:[path absoluteString]])
{
NSWorkspace *ws=[NSWorkspace sharedWorkspace];
[ws openURL:path];
}
The code to check if a path is mounted is:
-(BOOL)isMountedPath:(NSString *)share
{
NSArray * keys = #[NSURLVolumeURLForRemountingKey];
NSArray * mountPaths = [[NSFileManager defaultManager] mountedVolumeURLsIncludingResourceValuesForKeys:keys options:0];
NSError * error;
NSURL * remount;
for (NSURL * mountPath in mountPaths) {
[mountPath getResourceValue:&remount forKey:NSURLVolumeURLForRemountingKey error:&error];
if(remount){
if ([[[NSURL URLWithString:share] host] isEqualToString:[remount host]] && [[[NSURL URLWithString:share] path] isEqualToString:[remount path]])
{
printf("Already mounted at %s\n", [[mountPath path] UTF8String]);
return YES;
}
}
}
return NO;
}
Other possible useful method is:
-(NSString *)mountedPath:(NSString *)share
{
NSArray * keys = #[NSURLVolumeURLForRemountingKey];
NSArray * mountPaths = [[NSFileManager defaultManager] mountedVolumeURLsIncludingResourceValuesForKeys:keys options:0];
NSError * error;
NSURL * remount;
for (NSURL * mountPath in mountPaths) {
[mountPath getResourceValue:&remount forKey:NSURLVolumeURLForRemountingKey error:&error];
if(remount){
if ([[[NSURL URLWithString:share] host] isEqualToString:[remount host]] && [[[NSURL URLWithString:share] path] isEqualToString:[remount path]])
{
printf("Already mounted at %s\n", [[mountPath path] UTF8String]);
return [mountPath path];
}
}
}
return nil;
}

Resources