Cocoa : Refresh NSObjectController after unarchiving Data from disk - cocoa

I have an Object bound to the user interface with a NSObjectController. I am able to archive the Object and unarchive it later. This works fine so far. In the Debugger I can see the object holds the data I saved in a previous session. The remaining problem is: The user interface does not refresh. I guess I have to tell the NSObjectController somehow he has to deal with an other object. But I don't know how. I tried newObject but that did not work at all.
At the moment my code looks like this:
if ([aOpenPanel runModal] == NSOKButton)
{
NSString *filename = [aOpenPanel filename];
rpgCharacter = [NSKeyedUnarchiver unarchiveObjectWithFile:filename];
// [myCharacterController DoSomething] ???
}
rpgCharacter should be the object for the myCharacterController.

What you are doing is setting the rpgCharacter iVar directly. In order to trigger KVO you need to do this in a different way either:
if you are using Objective-C 2.0 and property syntax:
if ([aOpenPanel runModal] == NSOKButton)
{
NSString *filename = [aOpenPanel filename];
self.rpgCharacter = [NSKeyedUnarchiver unarchiveObjectWithFile:filename];
}
or, if you are using KVC directly and have a correctly named setter:
if ([aOpenPanel runModal] == NSOKButton)
{
NSString *filename = [aOpenPanel filename];
[self setRpgCharacter:[NSKeyedUnarchiver unarchiveObjectWithFile:filename]];
}

Related

Core Data Exporting all tables

I have an app that creates individual events and stores them in core data. What I need to do it load one individually and then export it by email. The code below works except it exports every event where I need it to just export the index path selected one. The code does load the appropriate record because the NSLog (#"My record is: %#", currentItem); does display only the settings for that event but when the data is exported to email all events are sent. I need the selected event with the event name to export. Any thoughts?
NSInteger index = exportevent.tag;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
CDBaseItem *rawRecord = [self.fetchedResultsController objectAtIndexPath:indexPath];
CDSurveyItem *surveyItem = [CDSurveyItem castObject:rawRecord];
self.recordEditID = [rawRecord.objectID URIRepresentation];
NSManagedObjectID *objectId = [self.managedObjectContext.persistentStoreCoordinator managedObjectIDForURIRepresentation:self.recordEditID];
TSPItem *currentItem = [self.managedObjectContext objectWithID:objectId];
NSString *eventName = nil;
if (currentItem.eventname) {
eventName = currentItem.eventname;
}
else if (surveyItem.eventname) {
eventName = surveyItem.eventname;
}
[self setSelection:indexPath];
if (self.selection)
{
if (currentItem)
{
NSLog (#"My record is: %#", currentItem);
NSData *export = [CDJSONExporter exportContext:currentItem.managedObjectContext auxiliaryInfo:nil];
MFMailComposeViewController *composeVC1 = [[MFMailComposeViewController alloc] init];
composeVC1 = [[MFMailComposeViewController alloc] init];
composeVC1.mailComposeDelegate = self;
[composeVC1 setSubject:[NSString stringWithFormat:#"Settings From %# Event", eventName]];
[composeVC1 setMessageBody:[NSString stringWithFormat:#"Here is the event settings. Simply press on the attachment and then choose Open in iPIX"] isHTML:NO];
[composeVC1 addAttachmentData:export mimeType:#"application/octet-stream" fileName:[NSString stringWithFormat:#"%#.ipix", eventName]];
[self presentViewController:composeVC1 animated:NO completion:^(void){}];
}
[self setSelection:nil];
}
Your NSLog may be correct, but you're not exporting the thing that you're printing. In this line (which I assume is a reference to this project):
NSData *export = [CDJSONExporter exportContext:currentItem.managedObjectContext auxiliaryInfo:nil];
You're telling CDJSONExporter to export the context, not a single object. You get every object because that is what CDJSONExporter does. It gets everything it can find in the context and gives you a data object. It's not designed to do what you're asking it to do.
If you want to convert a single object to JSON, you could
Roll your own JSON conversion code. Since you know what the object looks like, this would be easy. Or...
Implement Encodable on your model object and then use JSONEncoder to convert to JSON. Or...
Find some other open source project that does what you want, instead of this one which does not.

How do I override drag reception in an NSTextField?

I'm writing this in Swift. I have an NSTextField I've assigned a class in IB defined by:
class MyTextField : NSTextField, NSDraggingDestination {
I've overridden draggingEntered, draggingUpdated, prepareForDragOperation, performDragOperation in the subclass, but none of these is ever called and the system just puts stuff in the field as it sees fit. I want to handle the drag because, among other things, I don't want the default behavior of pasting a URL into the field if the user drags a file to it. Instead, if he does that, I want to get the display name of the file and use that instead.
What am I missing?
One of the responsibilities of any object implementing the <draggingDestination> protocol is to maintain an array of data-types which informs others what sort of data will trigger the methods you mention in your question. To allow your subclass to deal with drags from Finder or the desktop, I've found you need to register for three pasteboard types.
/* Sorry, not using Swift yet */
// MyNSTextField.m
- (void)awakeFromNib {
[self registerForDraggedTypes:#[NSPasteboardTypeString,
NSURLPboardType,
NSFilenamesPboardType]];
}
At least on OS X 10.9, this is sufficient to fire your draggingEntered method. If all you want on the pasteboard is the filename, rather than the full URL or path, you need to (i) extract the name, (ii) clear the pasteboard and (iii) add just the name back onto the pasteboard:
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
NSDragOperation operation = NSDragOperationNone;
NSPasteboard *pBoard = [sender draggingPasteboard];
NSArray *array = [pBoard readObjectsForClasses:#[[NSURL class], [NSString class]]
options:nil];
if ([array count] > 0) {
NSString *filename;
if ([[array firstObject] isKindOfClass:[NSURL class]]) {
// Possibly a file dragged from Finder
NSURL *url = [array firstObject];
filename = [[url pathComponents] lastObject];
} else if ([[array firstObject] isKindOfClass:[NSString class]]) {
// Possibly a file dragged from the desktop
NSString *path = [array firstObject];
BOOL isPath = [[NSFileManager defaultManager] fileExistsAtPath:path];
if (isPath) {
filename = [path lastPathComponent];
}
}
if (filename) {
[pBoard clearContents];
[pBoard setData:[filename dataUsingEncoding:NSUTF8StringEncoding]
forType:NSPasteboardTypeString];
operation = NSDragOperationGeneric;
}
}
return operation;
}
On occasion the drag into the text field will happen so quickly that the above method is not triggered, in which case you're back to the same problem. One way around this is to implement the following text field delegate method:
// From NSTextFieldDelegate Protocol
- (void)textDidChange:(NSNotification *)notification
In this method you can compare the contents of your text field, with the contents of the pasteboard, if you're field now contains a valid system path and this path matches the contents of the pasteboard, you know you need to adjust the string in the text field. Fortunately, this seems to happen so quickly that it looks just like a normal paste operation.

EXC_BAD_ACCESS while working with Core Data

I'm new into Cocoa and am writing a simple app to learn working with Core Data, but it crashes with EXC_BAD_ACCESS. Tried several things and haven't find the solution yet. As I said, I'm not very experienced in Cocoa.
I have followed the usual Core Data tutorials.
This is my Model:
I've added these two entities as NSArrayController in my Nib file and have two NSTableViews with Value Binding to the entity objects.
And here's the code:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSManagedObjectContext *context = [self managedObjectContext];
TaskList *list = [NSEntityDescription
insertNewObjectForEntityForName:#"TaskList"
inManagedObjectContext: context]; // EXC_BAD_ACCESS happens here
[list setTitle:#"Inbox"];
Task *task = [NSEntityDescription
insertNewObjectForEntityForName:#"Task"
inManagedObjectContext: context];
[task setKey:#"Remember the milk"];
[task setList:list];
NSError *error;
if (![context save:&error]) {
NSLog(#"Error: %#", [error localizedDescription]);
}
}
That's it! That's all my program. I am using Xcode 4.2, developing a Mac app, and ARC is enabled.
UPDATE: jrturton asked me to include implementation of [self managedObjectContext]. I didn't write this code, but here's what I found in AppDelegate.h:
#property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
And this is from AppDelegate.m:
#synthesize managedObjectContext = __managedObjectContext;
...
/**
Returns the managed object context for the application (which is already
bound to the persistent store coordinator for the application.)
*/
- (NSManagedObjectContext *)managedObjectContext {
if (__managedObjectContext) {
return __managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setValue:#"Failed to initialize the store" forKey:NSLocalizedDescriptionKey];
[dict setValue:#"There was an error building up the data file." forKey:NSLocalizedFailureReasonErrorKey];
NSError *error = [NSError errorWithDomain:#"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
[[NSApplication sharedApplication] presentError:error];
return nil;
}
__managedObjectContext = [[NSManagedObjectContext alloc] init];
[__managedObjectContext setPersistentStoreCoordinator:coordinator];
return __managedObjectContext;
}
Check your managed object model. Make sure all the entity and attribute names are spelled correctly. Also check your object class files and make sure they contain what you expect.
Maybe the debugger does not show you the correct row when crashing: I noticed, that you have a method setKey:, but no attribute called keyin your Task entity. Try setting all the attributes with the dot notation, like list.title = #"Inbox". (This is generally easier to read and avoids similar errors.)
As suggested, before the line inserting the new entity, set a breakpoint and make sure the managed object context is not null.
Finally, perhaps you have to cast your object. insertNewObjectForEntityForName: returns an object of type NSManagedObject, but you are assigning it to a type TaskList. Try adding the cast before you use the object:
TaskList *list = (TaksList *) [NSEntityDescription
insertNewObjectForEntityForName:#"TaskList"
inManagedObjectContext: context];
I had this same issue. I resolved it like Mostafa said above. If you create a project with Core Data enabled, it will automatically create a file for you. Use this .xcdatamodeld file instead of a custom one. If you have one already created, just delete the originally created file and rename your datamodel file to the originally created file name.

What's the most common scenario for Cocoa app setup during first launch?

I am creating an app and I would like a user to set some obligatory preferences during first app launch. What is the most common scenario to achieve this? Should I set some user defaults to see if the app has been setup? Also - if I determine that the app is being launched for the first time - how should I display "Setup" window? If I load it from the separte xib file - how will I deffer the display of main app window?
The standard way to do this is in the +(void)initialize method of your main controller class.
For example, in your interface (.h):
#interface MDAppController : NSObject {
BOOL MDFirstRun;
BOOL showInspector;
BOOL showIcons;
}
#end
Then in your .m file:
NSString * const MDFirstRunKey = #"MDFirstRun";
NSString * const MDShouldShowInspectorKey = #"MDShouldShowInspector";
NSString * const MDBrowserShouldShowIconsKey = #"MDBrowserShouldShowIcons";
#implementation
+ (void)initialize {
NSMutableDictionary *defaultValues = [NSMutableDictionary dictionary];
[defaultValues setObject:[NSNumber numberWithBool:YES]
forKey:MDFirstRunKey];
[defaultValues setObject:[NSNumber numberWithBool:NO]
forKey:MDShouldShowInspectorKey];
[defaultValues setObject:[NSNumber numberWithBool:YES]
forKey:MDBrowserShouldShowIconsKey];
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultValues];
[[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:defaultValues];
}
line break
- (id)init {
if (self = [super init]) {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
MDFirstRun = [[userDefaults objectForKey:MDFirstRunKey] boolValue];
showInspector = [[userDefaults objectForKey:MDShouldShowInspectorKey] boolValue];
showIcons = [[userDefaults objectForKey:MDBrowserShouldShowIconsKey] boolValue];
}
return self;
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
if (MDFirstRun) {
[[NSUserDefaults standardUserDefaults]
setObject:[NSNumber numberWithBool:NO]
forKey:MDFirstRunKey];
// show first use panel
} else {
// do normal launch
}
}
/// other methods
#end
Basically, you set up all of your default values in your initialize method. (The initialize method is called very early on before init is called, so it provides a convenient place to make sure user defaults all have default values). The registerDefaults: method of NSUserDefaults is special in that the values you pass in only are used if a particular value hasn't already been set. In other words, when in the code above, I set the first launch key to NO in the applicationDidFinishLaunching: method, that overrides the default value and will be saved to your application's preferences plist file. The values that are saved in the preferences file take precedence over those that you've registered with user defaults in the initialize method.
To defer opening of the main window, you basically want to make sure that the "Visible at Launch" flag is turned off for the window in question in the Attributes inspector in Interface Builder:
It's that flag that determines whether a window is shown as soon as the nib is loaded, or whether you will need to do it programmatically using something like makeKeyAndOrderFront:.

NSManagedObjectContext save causes NSTextField to lose focus

This is a really strange problem I'm seeing in my app. I have an NSTextField bound to an attribute of an NSManagedObject, but whenever the object is saved the textfield loses focus. I'm continuously updating the value of the binding, so this is far from ideal.
Has anyone seen anything like this before, and (hopefully) found a solution?
I encountered the issue recently and fixed it by changing the way the NSTextField was bound to the NSManagedObject attribute. Instead of binding the value of the text field to the selection.[attribute] key path of the NSArrayController, I bound the arrayController.selection.[attribute] keyPath of the view controller that had a proper outlet pointing to the controller.
For some reason, the NSTextField doesn't loose focus when the NSManagedObjectContext is saved if bound this way.
I want to share my solution. It will work for all fields without modification.
I have optimized it for this posting and removed some error checking, logging and thread safety.
- (BOOL)saveChanges:(NSError **)outError {
BOOL result = YES;
#try {
NSError *error = nil;
if ([self hasChanges]) {
// Get field editor
NSResponder *responder = [[NSApp keyWindow] firstResponder];
NSText *editor = [[NSApp keyWindow] fieldEditor: NO forObject: nil];
id editingObject = [editor delegate];
BOOL isEditing = (responder == editor);
NSRange range;
NSInteger editedRow, editedColumn;
// End editing to commit the last changes
if (isEditing) {
// Special case for tables
if ([editingObject isKindOfClass: [NSTableView class]]) {
editedRow = [editingObject editedRow];
editedColumn = [editingObject editedColumn];
}
range = [editor selectedRange];
[[NSApp keyWindow] endEditingFor: nil];
}
// The actual save operation
if (![self save: &error]) {
if (outError != nil)
*outError = error;
result = NO;
} else {
result = YES;
}
// Now restore the field editor, if any.
if (isEditing) {
[[NSApp keyWindow] makeFirstResponder: editingObject];
if ([editingObject isKindOfClass: [NSTableView class]])
[editingObject editColumn: editedColumn row: editedRow withEvent: nil select: NO];
[editor setSelectedRange: range];
}
}
} #catch (id exception) {
result = NO;
}
return result;
}
OK, so thanks to Martin for pointing out that I should read the docs a little more closely. This is expected behaviour, and here's what I did to get around it (use your judgement as to whether this is appropriate for you):
I save my context once every 3 seconds, checking at the start if the context has any changes before I bother executing the actual save: method on my NSManagedObjectContext. I added a simple incrementing/decrementing NSUInteger (_saveDisabler) to my Core Data controller class that is modified via the following methods:
- (void)enableSaves {
if (_saveDisabler > 0) {
_saveDisabler -= 1;
}
}
- (void)disableSaves {
_saveDisabler += 1;
}
Then all I do in my custom saveContext method is do a simple check at the top:
if (([moc hasChanges] == NO) || (_saveDisabler > 0)) {
return YES;
}
This prevents the save from occurring, and means that the focus is not stolen from any of my custom textfield subclasses. For completeness, I also subclassed NSTextField and enable/disable saves in my Core Data controller from the following methods:
- (void)textDidBeginEditing:(NSNotification *)notification;
- (void)textDidEndEditing:(NSNotification *)notification;
It might be a little messy, but it works for me. I'm keen to hear of cleaner/less convoluted methods if anyone has done this successfully in another way.

Resources