adding a Core Data object from a segue - xcode

in getting familiar with core data i have found myself puzzled by the question of what to pass various view controllers (VCs) when trying to add data.
for example, in the CoreDataRecipes project that apple provides as an example (http://developer.apple.com/library/ios/#samplecode/iPhoneCoreDataRecipes/Introduction/Intro.html) they use the following approach
when the user wants to add a recipe to the list of recipes presented in the master table view, and hits the Add button, the master table view controller (called RecipeListTableViewController) creates a new managed object (Recipe) as follows:
- (void)add:(id)sender {
// To add a new recipe, create a RecipeAddViewController. Present it as a modal view so that the user's focus is on the task of adding the recipe; wrap the controller in a navigation controller to provide a navigation bar for the Done and Save buttons (added by the RecipeAddViewController in its viewDidLoad method).
RecipeAddViewController *addController = [[RecipeAddViewController alloc] initWithNibName:#"RecipeAddView" bundle:nil];
addController.delegate = self;
Recipe *newRecipe = [NSEntityDescription insertNewObjectForEntityForName:#"Recipe" inManagedObjectContext:self.managedObjectContext];
addController.recipe = newRecipe;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController];
[self presentModalViewController:navigationController animated:YES];
[navigationController release];
[addController release];
}
this newly created object (a Recipe) is passed to the RecipeAddViewController. the RecipeAddViewController has two methods, save and cancel, as follows:
- (void)save {
recipe.name = nameTextField.text;
NSError *error = nil;
if (![recipe.managedObjectContext save:&error]) {
/*
Replace this implementation 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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.delegate recipeAddViewController:self didAddRecipe:recipe];
}
- (void)cancel {
[recipe.managedObjectContext deleteObject:recipe];
NSError *error = nil;
if (![recipe.managedObjectContext save:&error]) {
/*
Replace this implementation 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. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.delegate recipeAddViewController:self didAddRecipe:nil];
}
i am puzzled about this design approach. why should the RecipeListViewController create the object before we know if the user wants to actually enter a new recipe name and save the new object? why not pass the managedObjectContext to the addRecipeController, and wait until the user hits save to create the object and populate its fields with data? this avoids having to delete the new object if there is no new recipe to add after all. or why not just pass a recipe name (a string) back and forth between the RecipeListViewController and the RecipeAddController?
i'm asking because i am struggling to understand when to pass strings between segues, when to pass objects, and when to pass managedObjectContexts...
any guidance much appreciated, incl. any links to a discussion of the design philosophies at issue.

Your problem is that NSManagedObjects can't live without a context. So if you don't add a Recipe to a context you have to save all attributes of that recipe in "regular" instance variables. And when the user taps save you create a Recipe out of these instance variables.
This is not a huge problem for an AddViewController, but what viewController do you want to use to edit a recipe? You can probably reuse your AddViewController. But if you save all data as instance variables it gets a bit ugly because first you have to get all data from the Recipe, save it to instance variables, and when you are done you have to do the reverse.
That's why I usually use a different approach. I use an editing context for editing (or adding, which is basically just editing).
- (void)presentRecipeEditorForRecipe:(MBRecipe *)recipe {
NSManagedObjectContext *editingContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
editingContext.parentContext = self.managedObjectContext;
MBRecipe *recipeForEditing;
if (recipe) {
// get same recipe inside of the editing context.
recipeForEditing = (MBRecipe *)[editingContext objectWithID:[recipe objectID]];
NSParameterAssert(recipeForEditing);
}
else {
// no recipe for editing. create new one
recipeForEditing = [MBRecipe insertInManagedObjectContext:editingContext];
}
// present editing view controller and set recipeForEditing and delegate
}
Pretty straight forward code. It creates a new children context which is used for editing. And gets a recipe for editing from that context.
You must not save the context in your EditViewController! Just set all desired attributes of Recipe, but leave the context alone.
After the user tapped "Cancel" or "Done" this delegate method is called. Which either saves the editingContext and our context or does nothing.
- (void)recipeEditViewController:(MBRecipeEditViewController *)editViewController didFinishWithSave:(BOOL)didSave {
NSManagedObjectContext *editingContext = editViewController.managedObjectContext;
if (didSave) {
NSError *error;
// save editingContext. this will put the changes into self.managedObjectContext
if (![editingContext save:&error]) {
NSLog(#"Couldn't save editing context %#", error);
abort();
}
// save again to save changes to disk
if (![self.managedObjectContext save:&error]) {
NSLog(#"Couldn't save parent context %#", error);
abort();
}
}
else {
// do nothing. the changes will disappear when the editingContext gets deallocated
}
[self dismissViewControllerAnimated:YES completion:nil];
// reload your UI in `viewWillAppear:`
}

Related

Unable to use reference from a Make New command in Core-data app with AppleScript support

I am able to support the Make New command of AppleScript for my app, however the returned 'specified object' (an NSUniqueIDSpecifier) for the core data managed object is useless. The following AppleScript returns the error message:
error "SpellAnalysis got an error: Invalid key form." number -10002 from level id "x-coredata:///Levels/tC5A49E01-1CE1-4ED6-8F6B-BC0AE90E279A2"
tell application "SpellAnalysis"
set thisLevel to make new «class Slev» with properties {«class Saln»:3}
get properties of thisLevel
end tell
So the newly created Levels object can not be acted upon in AppleScript. I've combed the Web for a solution to this and the closest thing I have found is Bill Cheeseman's example app, "WareroomDemo" which specifically deals with Cocoa Scriptability for Core Data apps (the Sketch example does not use Core Data). Unfortunately, it is a dated example, running only on pre-64-bit XCode and I can't actually run it--I can only look at the code. His app's Make Command may have the same limitations for all I know.
The returned 'objectSpecifier' is unable to refer to the created object either as a safe-guard against corrupting Core Data's organizing scheme, or perhaps because the returned object is an un-cashed 'fault'. I think the latter possibility is unlikely because I can force the fault to cash (by getting a property value on the managed object) , yet I get the same error message with the AppleScript.
Here is the method that creates my class:
- (id)newScriptingObjectOfClass:(Class)class forValueForKey:(NSString *)key withContentsValue:(id)contentsValue properties:(NSDictionary *)properties { // Creates a new Lesson object in response to the AppleScript 'make' command.
// Documentation for 'newScriptingObject…' states that to create a new class object when using Core Data, you intercede using the following method (or you can subclass the NSCreateCommand's 'performDefaultImplementation' method and put your NSManagedObject init code there):
if (class == [Levels class]) {
//NSLog(#"class: %#",class);
NSEntityDescription *levelsEntity = [NSEntityDescription
entityForName:#"Levels"
inManagedObjectContext:levelsDBase];
NSManagedObject *levelObject = [[NSManagedObject alloc] initWithEntity:levelsEntity insertIntoManagedObjectContext:levelsDBase];
SLOG(#"lessonObject: %#", lessonObject);
NSString *levelNumberString = [[properties objectForKey:#"levelNumber"] stringValue];
SLOG(#"levelNumberString: %#", levelNumberString);
[levelObject setValue:levelNumberString forKey:#"levelNumber"];
return levelObject; // When using Core Data, it seems that you return the newly created object directly
}
return [super newScriptingObjectOfClass:(Class)class forValueForKey:(NSString *)key withContentsValue:(id)contentsValue properties:(NSDictionary *)properties];
}
Here is my object specifier method:
- (NSScriptObjectSpecifier *)objectSpecifier {
// This NSScriptObjectSpecifiers informal protocol returns a unique ID specifier specifying the absolute string of the URI representation of this managed object. // AppleScript return value: 'level id <id>'.
// The primary container is the application.
NSScriptObjectSpecifier *containerRef = nil; // I understand that if the application is the container, this is value you use for the container reference
NSString *uniqueID = [[[self objectID] URIRepresentation] absoluteString];
return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:[NSScriptClassDescription classDescriptionForClass:[NSApp class]] containerSpecifier:containerRef key:#"levelsArray" uniqueID:uniqueID] autorelease];
}
The problem lies with the specifier method. The Sketch example actually uses the technique that I needed. I overlooked it many times because I didn't see how it would apply to Core Data managed objects. Instead of returning the objects uniqueID, you make it return the managedObject index using the 'indexOfObjectIdenticalTo:' method as follows:
- (NSScriptObjectSpecifier *)objectSpecifier {
NSArray *levelsArray = [[NSApp delegate] levelsArray]; // Access your exposed to-many relationship--a mutable array
unsigned index = [levelsArray indexOfObjectIdenticalTo:self]; // Determin the current objects index
if (index != (unsigned)NSNotFound) {
// The primary container is the document containing this object's managed object context.
NSScriptObjectSpecifier *containerRef = nil; // the appliation
return [[[NSIndexSpecifier allocWithZone:[self zone]] initWithContainerClassDescription:[NSScriptClassDescription classDescriptionForClass:[NSApp class]] containerSpecifier:containerRef key:#"levelsArray" index:index] autorelease];
} else {
return nil;
}
}
Note that this method resides within a subclass of your Core Data managedObject--in this case, the 'Levels' class. The 'self' within the 'indexOfObjectIndenticalToSelf:' method refers to the current managedObject ('Levels') being handled. Also, be sure to provide the specifier (accessor) type to your 'sdef' file, like this:
<element type="level">
<cocoa key="levelsArray"/>
<accessor style="index"/>
</element>

RestKit 0.20 — What is the preferred way to create a new NSManagedObject?

I'm curious to know what the best way is to create a new NSManagedObject in RestKit 0.20? Currently my code looks something like this:
#pragma mark - navigation buttons
- (void)createButtonDidTouch
{
// create new album object
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
NSManagedObjectContext *parentContext = RKObjectManager.sharedManager.managedObjectStore.mainQueueManagedObjectContext;
context.parentContext = parentContext;
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"Album" inManagedObjectContext:parentContext];
Album *newAlbum = [[Album alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:context];
// pass object to create view to manipulate
AlbumCreateViewController *createViewController = [[AlbumCreateViewController alloc] initWithData:newAlbum];
createViewController.delegate = self;
createViewController.managedObjectContext = context;
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:createViewController];
navController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentViewController:navController animated:YES completion:nil];
}
#pragma mark - create view controller delegate
- (void)createViewControllerDidSave:(NSManagedObject *)data
{
// dismiss the create view controller and POST
// FIXME: add restkit code to save the object
NSLog(#"save the object...");
NSDictionary *userInfo = [KeychainUtility load:#"userInfo"];
NSString *path = [NSString stringWithFormat:#"/albums/add/%#/%#", userInfo[#"userID"], userInfo[#"apiKey"]];
[RKObjectManager.sharedManager postObject:data path:path parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
operation.targetObject = data;
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"create album error: %#", error);
}];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)createViewControllerDidCancel:(NSManagedObject *)data
{
// dismiss the create view controller
NSLog(#"delete the object...");
// FIXME: add restkit code to delete the object
[self dismissViewControllerAnimated:YES completion:nil];
}
I'm also curious to know what my responsibilities are for saving / deleting this object. If I POST to the server via RestKit is the managed object context saved?
What if I decide to cancel this creation process — what's the preferred way to delete this object?
Basically how much is RestKit doing for me, and what should I make sure I'm doing. I haven't found much documentation on this and would like to be clear on it.
When you initialize an RKManagedObjectRequestOperation for a given object, RestKit will obtain a permanent object ID for that object and then create a child managed object context whose parent context is the context the object is inserted into. The operation then executes the HTTP request to completion and obtains a response.
If the response is successful and the mapping of the response is successful (note that the mapping occurs within this private child context), then the private child context is saved. The type of save invoked is determined by the value of the savesToPersistentStore property (see http://restkit.org/api/0.20.0/Classes/RKManagedObjectRequestOperation.html#//api/name/savesToPersistentStore).
When YES, the context is saved recursively all the way back to the persistent store via the NSManagedObjectContext category method saveToPersistentStore (see http://restkit.org/api/0.20.0/Categories/NSManagedObjectContext+RKAdditions.html).
When NO, the context is saved via a vanilla [NSManagedObjectContext save:] message, which 'pushes' the changes back to the parent context. They will remain local to that context until you save them back. Keep in mind that managed object context parent/child hierarchies can be as long as you create within the application.
If the HTTP request failed or there was an error during the mapping process, the private context is not saved and the operation is considered failed. This means that no changes are saved back to the original MOC, leaving your object graph just as it was before the operation was started (except the object being sent, if temporary, now has a permanent object ID but is still unsaved).
The way you do it should works (calling each time the MOC in each of your VC), but is not "recommended".
What Apple suggests, just like any Core Data app, is the "pass the baton" style.
Nested contexts make it more important than ever that you adopt the
“pass the baton” approach of accessing a context (by passing a context
from one view controller to the next) rather than retrieving it
directly from the application delegate.
See here:
http://developer.apple.com/library/ios/#releasenotes/DataManagement/RN-CoreData/_index.html
As for your second question, RestKit should manage saving/updating your Core Data stack upon success of your api calls if everything is well mapped/setup.
From blake the RK creator:
if you POST or PUT a Core Data object, RK obtains a permanent object
ID for it and then creates a secondary managed object context, fires
the request, and maps the response (if possible). if the response and
the mapping are successful, it will either save it back to the parent
context or all the way back to the persistent store (i.e. into SQLite)
based on the value of the savesToPersistentStore.

Promising files from my view-based NSTableView

I'm writing a Mac app with a view-based table view. It's a list of images, which I intend for the user to be able to drag to the Finder to save each image to a file.
The data source owns an array of custom model objects. The model objects all conform to the NSPasteboardWriting protocol as follows:
- (NSArray *)writableTypesForPasteboard:(NSPasteboard *)pasteboard {
//self.screenshotImageDataType is set after the image is downloaded, by examining the data with a CGImageSource.
//I have verified that it is correct at this point. Its value in my test was #"public.jpeg" (kUTTypeJPEG).
return #[ self.screenshotImageDataType, (__bridge NSString *)kUTTypeURL, (__bridge NSString *)kPasteboardTypeFilePromiseContent ];
}
- (id)pasteboardPropertyListForType:(NSString *)type {
if (UTTypeEqual((__bridge CFStringRef)type, (__bridge CFStringRef)self.screenshotImageDataType)) {
return self.screenshotImageData;
} else if (UTTypeEqual((__bridge CFStringRef)type, kUTTypeURL)) {
return [self.screenshotImageURL pasteboardPropertyListForType:type];
} else if (UTTypeEqual((__bridge CFStringRef)type, kPasteboardTypeFilePromiseContent)) {
return self.screenshotImageDataType;
}
id plist = [self.screenshotImage pasteboardPropertyListForType:type]
?: [self.screenshotImageURL pasteboardPropertyListForType:type];
NSLog(#"plist for type %#: %# %p", type, [plist className], plist);
return [self.screenshotImage pasteboardPropertyListForType:type]
?: [self.screenshotImageURL pasteboardPropertyListForType:type];
}
The URLs that my objects own are web URLs, not local files. They're the URLs the images were downloaded from.
The table view's data-source-and-delegate-in-one implements a method relating to file promises:
- (NSArray *)tableView:(NSTableView *)tableView
namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestinationURL
forDraggedRowsWithIndexes:(NSIndexSet *)rows
{
return [[self.screenshots objectsAtIndexes:rows] valueForKeyPath:#"screenshotImageURL.lastPathComponent"];
}
Logging the value of that expression produces a valid filename with the correct filename extension.
Finally, in windowDidLoad, I have sent the message that turns this whole mess on:
//Enable copy drags to non-local destinations (i.e., other apps).
[self.tableView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
The stage is set. Here's what happens when the curtains go up:
When I drag to a window owned by the Finder, the view I'm dragging to highlights, indicating that it will accept the drag.
However, when I drop the image, no file is created.
Why doesn't the file whose contents I've promised get created?
The documentation from the NSDraggingInfo protocol's namesOfPromisedFilesDroppedAtDestination: method gives a hint:
The source may or may not have created the files by the time this method returns.
Apparently, at least in table view context, this translates to “create the files yourself, you lazy bum”.
I amended my table view data source's tableView:namesOfPromisedFilesDroppedAtDestination:forDraggedRowsWithIndexes: method to tell each dragged model object to write itself to a file (consisting of the destination directory URL + the filename from the model object's source URL), and implemented that functionality in the model object class. All works now.

NSFetchResultController: Only Initial Load shows no data in the tableViewController's table

I have:
#interface CMainTableViewController : UITableViewController
In that class I have an NSFetchResultController.
I have the following separate data manager class:
DB_ManagerSingleton
//Here I have a UIManagedDocument that creates the DB/persistent store
To open my persistent store I do the following from my DB_ManagerSingleton class:
UIManagedDocument *fileDB = [[UIManagedDocument alloc]initWithFileURL:url];
if([[NSFileManager defaultManager] fileExistsAtPath:self.fileDB.fileURL.path] != TRUE)
{
[self.fileDB saveToURL:self.fileDB.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success)
{
[self completeDBSetup:success];
}];
}//End if
else if(self.fileDB.documentState == UIDocumentStateClosed)
{
[self.fileDB openWithCompletionHandler:^(BOOL success)
{
[self completeDBSetup:success];
}];
}//End else if
else if(self.fileDB.documentState == UIDocumentStateNormal)
{
[self completeDBSetup:TRUE];
}//End else if
The completeDBSetup places 4 NSManageObjects into the NSManagedObjectContext and notifies the registered observers of the success that the persistent store was opened:
if([CLevelFactory createLevels] == TRUE)
[self.fileDB.managedObjectContext save:&error];
for(id<DB_StateNotification> observer in self.arrNotificationDelegates)
{
if(observer != nil)
[observer DB_StateChange:self.isDBSetup];
}//End for loop
Now, in this case the observer is my TableViewController class. Within this class I have implemented all the methods in the NSFetchedResultsControllerDelegate and the implementation is copied exactly from the Coredata books example.
http://developer.apple.com/library/ios/#samplecode/CoreDataBooks/Listings/Classes_RootViewController_m.html#//apple_ref/doc/uid/DTS40008405-Classes_RootViewController_m-DontLinkElementID_14
The way I set up my tableView controller in the DB_StateChange method from above is as follows:
self.fetchResultController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:dbMgr.fileDB.managedObjectContext
sectionNameKeyPath:#"levelID"
cacheName:nil];
self.fetchResultController.delegate = self;
NSError *error = nil;
self.bQuerierYourData = [self.fetchResultController performFetch:&error];
NSArray *fetchedObjects = [self.fetchResultController fetchedObjects];
//NOTE: I ALWAYS have a count greater than 0. Even on the first run.
if(fetchedObjects.count > 0)
[self.tableView reloadData];
Now the issue is this:
The very first time my application launches I get squat in my table view. I get a fetchedObjects.count of 4 - which is what I expect, but not'a in the table view. Now, without touching anything. Not inserting, updating, or deleting anything in the persistent store, I just quick the application. In this case hitting the stop button on the debugger. When I go ahead and restart the app, everything comes in beautifully. I don't insert data every time the app starts, I test and if the data already exists, I just move on without inserting any data.
Does anyone have any idea what is going on here?

NSArrayController creating, modifying and then selecting a new object

The main view of my NSPersistentDocument based application is a table view (bound to an NSArrayController) showing the list of records, below it there is an "add record" button. I want the button to cause the following (supposedly trivial) behavior.
Create an new object
Set some defaults to the new object (that are stored in the main document and not available globally)
Add it to the table view.
Here are the things that I tried or dismissed:
use the NSArrayController "add" action - problem: will not return the new object and implementation is deferred so it is impossible to modify the newly created object
Override the init of the data class - will not work - I need to access data that is stored in the document class instance
Subclass NSArrayController and override "newObject" - again - will not work because I need to access data that is stored in the document.
Following code "almost" worked:
- (IBAction)newRecord:(id)sender
{
MyDataClass *newRecord = [recordsArrayController newObject];
newRecord.setting1=self.defaultSetting1;
newRecord.setting2=self.defaultSetting2;
// ... etc...
[recordsArrayController addObject:newRecord];
[recordsTable scrollRowToVisible:[recordsTable selectedRow]];
[newRecord release];
}
This code actually works well, for unsaved documents. But if I save the document and re-open it then clicking on the add button results in the new record showing twice in the table.
Obviously the "addObject" is redundant (although it works fine in unsaved documents) but without it the new object is not selected.
Simple case that should work:
MyDataClass *newRecord = [controller newObject];
// configure newRecord
[controller addObject:newRecord];
[newRecord release];
In order for the new object to be selected, the controller needs to have been configured for -setSelectsInsertedObjects:YES previously.
But, there's an alternative which I'd consider more proper. Subclass NSArrayController like so (slighty pseudo-code):
#interface MyRecordController : NSArrayController
#property id recordSetting1;
#property id recordSetting2;
#end
#implementation MyRecordController
#synthesize recordSetting1;
#synthesize recordSetting2;
- (id)newObject
{
id result = [super newObject];
newRecord.setting1 = self.recordSetting1;
newRecord.setting2 = self.recordSetting2;
return result;
}
#end
So, your code then becomes:
- (IBAction)newRecord:(id)sender
{
recordsArrayController.recordSetting1 = self.defaultSetting1;
recordsArrayController.recordSetting2 = self.defaultSetting2;
[recordsArrayController add:self];
}
Really, all you need to do is modify your code to omit the addObject: call. To make your new object selected, just do this:
[recordsArrayController setSelectedObjects:[NSArray arrayWithObject:newObject]];
before you do your call to swcrollRowToVisible:. You're right that the addObject: call is unneeded. As you've seen, it's ending up in the array controller twice.
Also, you won't need to call [newRecord release]. The documentation says the object is retained by the array controller. It's not failing now because it's being retained a second time when you do addObject:.

Resources