Cannot Save in a Document-Based Application - cocoa

I created new Document-Based-App.
I implemented dataOfType in subclass of NSDocument
- (NSData*) dataOfType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
return [NSKeyedArchiver archivedDataWithRootObject:bcmwc.bindingsController.arrangedObjects];
}
in xib http://i.minus.com/iH2Rj9v5oOhTn.png
When I click "Save" from menu nothing's gonna happen, any errors in console.
I set a breakpoint in dataOfType, and when I clicked "Save", application didn't stop.
Any suggestions?
EDIT
I think it may be connected with fact I use custom NSWindowController, and custom xib.
I made a test when I load custom xib, everything is fine dataOfType method is invoked etc..
Should I connect in some way my custom xib (window) with subclass of NSDocument?

It looks like your Save menu item is connected properly, so let's concentrate on the code (+1 for having posted it in the first place).
You do nothing in your code to ensure -archivedDataWithRootObject: returns valid data or set an error if it doesn't. My best guess (because you haven't provided nearly enough detail to do anything but guess) is that you're returning nil because your call to -archivedDataWithRootObject: is doing the same. Check to see if you get valid data and set the outError if not.
Why would you get nil? Perhaps one or more of the objects in the object graph created by archiving your array controller's -arrangedObjects array isn't <NSCoding> compliant. This is likely the case if your array controller holds objects of a custom class you created rather than a standard Property List container.
Read the Archives and Serializations Programming Guide (in particular, the Encoding and Decoding Objects section) to learn how to make your custom class <NSCoding> compliant so that it knows how to serialize itself (write itself into a byte stream suitable for NSKeyedArchiver, etc. and create an instance of itself from such a byte stream).
Also, you really need to learn to use the debugger. You're groping around in a dark cave with lots of pitfalls and no flashlight without it. Try setting a breakpoint in methods you expect to be called. If they're not called, you can check outlets/actions, etc. If they are, you can step through each line and make sure everything is executing as you expected. If you write a little more verbose code, you can more easily inspect the results when paused in the debugger. Two lines in your case will help you more than one:
- (NSData*) dataOfType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
NSData * myData = [NSKeyedArchiver archivedDataWithRootObject:bcmwc.bindingsController.arrangedObjects];
// You should be handling nil (setting a descriptive error) here if (!myData)...
return data; // breakpoint here; you should now see myData is likely nil
}

Related

NSArrayController rearrangeObjects error

I have troubles with NSArrayController rearrangeObjects function - this function called from some background treads and sometimes App crashes with error: 'endUpdates called without a beginUpdates'.
How can i detect if arrayController is rearranging objects in current moment and add next rearrange to some like queue or cancel current rearranging and run new?
May be there is another solution for this problem?
Edit code added:
TableController implementation:
- (void)setContent{//perfoms on main thread
//making array of content and other functions for setting-up content of table
//...
//arrayController contains objects of MyNode class
//...
//end of setting up. Call rearrangeObjects
[arrayController rearrangeObjects];
}
- (void)updateItem:(MyNode *)sender WithValue:(id)someValue ForKey:(NSString *)key{
[sender setValue:someValue forKey:key];
[arrayController rearrangeObjects];//exception thrown here
}
MyNode implementation:
- (void)notifySelector:(NSNotification *)notify{
//Getted after some processing finished
id someValue = [notify.userInfo objectForKey:#"observedKey"];
[delegate updateItem:self WithValue:someValue ForKey:#"observedKey"];
}
Don't do that. AppKit (to which NSArrayController belongs) is not generally thread safe. Instead, use -performSelectorOnMainThread:... to update your UI (including NSArrayController). ALWAYS do updating on the main thread.
Joshua and Dan's solution is correct. It is highly likely that you are performing operations on your model object in a background thread, which then touches the array controller, and hence touches the table.
NSTableView itself is not threadsafe. Simply adding in a "beginUpdates/endUpdates" pair will just avoid the race condition for a bit. However, like Fin noted, it might be good to do the pair of updates for performance reasons, but it won't help with the crash.
To find the sources of the crash, add some assertions in your code on ![NSThread currentThread].mainThread -- particularly any places before you touch the array controller's content. This will help you isolate the problem. Or, subclass NSTableView and add the assertion in somewhere key, like overriding -numberOfRows (which is called frequently on changes), and calling super.
-corbin
AppKit/NSTableView
I solved this by adding this at the very start of my UI initialisation
[myTableView beginUpdates];
and then at the end after the persistant store has been loaded fully:
[myTableView endUpdates];
This also make the app startup a lot better since it does not constantly have to reload all loads from the MOC.

textShouldEndEditing does not get called in NSTableView

When a user adds a new managed object, it shows up in a table, which scrolls down to the new entry, and the name of the new object (a default value) goes into editing mode.
I need to check if the name of the new object is unique in the datastore, so I can't use a formatter for this. I think the perfect moment where I should validate this, is whenever the user tries to commit the entry's name value, using textShouldEndEditing:.
I subclassed NSTableView and overrid following methods, just to be able to check in the log if they get called.
- (BOOL)textShouldEndEditing:(NSText *)textObject {
NSLog(#"textSHOULDendEditing fired in MyTableView");
return [super textShouldEndEditing:textObject];
}
- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor {
NSLog(#"control:textShouldEndEditing fired in MyTableView");
return YES;
}
- (void)textDidEndEditing:(NSNotification *)aNotification {
NSLog(#"textDIDEndEditing fired in MyTableView");
}
textDidEndEditing: gets called fine, but textShouldEndEditing: does not.
In the NSTableView Class Reference, under Text Delegate Methods, both methods textShouldEndEditing: and textDidEndEditing: are listed. Someone please explain why one gets called and the other doesn't.
I think the NSTableView acts as the delegate for an NSTextField that gets instantiated as a black box delegate for the NSTextFieldCell. So what is referred to as delegate methods in the NSTableView Class Reference, actually implement the text manipulating methods for the NSTextField object.
I tried to declare the NSTextFieldCell as an outlet in my NSTableView. I also tried to declare several protocols in the NSTableView.
#import <AppKit/AppKit.h>
#import <Cocoa/Cocoa.h>
#interface MyTableView : NSTableView <NSTextDelegate, NSTextFieldDelegate, NSControlTextEditingDelegate, NSTableViewDelegate, NSTableViewDataSource> {
}
#end
Don't laugh, I even tried to declare my table view as its own delegate :P
After banging my head one entire day on this issue without finding any conclusive answer in Apple documentation, I decided to share the solution I've found in case somebody else struggles with the same problem.
According to the documentation, as the original poster mentioned, the methods control:textShouldBeginEditing and control:textShouldEndEditing of NSControlTextEditingDelegate should be called directly on the delegate:
This message is sent by the control directly to its delegate object.
Furthermore, a Technical Q&A was issued by Apple with the title Detecting the start and end edit sessions of a cell in NSTableView where it's clearly stated the following:
A: How do I detect start and end edit sessions of a cell in NSTableView?
In order to detect when a user is about to start and end an edit session of a cell in NSTableView, you need to be set as the delegate of that table and implement the following NSControl delegate methods:
- (BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)fieldEditor;
- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor;
The table forwards the delegate message it is getting from the text view on to your delegate object using the control:textShouldEndEditing: method. This way your delegate can be informed of which control the text view field editor is acting on its behalf.
I found nothing in Apple's documentation stating anything different and if someone does, a documentation pointer would really be appreciated.
In fact, this appears to be true if a cell-based NSTableView is being used. But as soon as you change the table to a view-based table, the delegate method is not called any longer on the table delegate object.
A Solution
However, some heuristic tests I performed showed that those delegate methods get called on a view-based table delegate if (and as far as I know: and only if):
The table delegate is set.
The delegate of the editable control is set.
If you remove either delegate, the methods of the NSControlTextEditingDelegate protocol will not be called.
What's unexpected according to the (only) documentation is setting the delegate of the editable control. On the other hand setting the delegate object to receive delegate notifications sounds rather intuitive to me, and that's why I tried in the first place. But there's a catch! The curious thing, though, is that that's not sufficient. If the table delegate is removed, the NSControlTextEditingDelegate methods will not be called even if the delegate of the editable control is set (which is the weirdest thing to me).
Hope this helps somebody else not to lose time on this issue.
in your question you mention the insertion of a "managed object" and that was the problem. It seems that you are using a view based table, but the textShouldEndEditing: method only gets called for cell based tables.
I overrid -(void)awakeFromInsert; in the (subclassed) managed object, to construct a unique default value for the name-property.
Also, I ended up not overriding the -(BOOL)textShouldEndEditing: method in the table view. Instead, I check if a newly entered name-property is unique in the (subclassed) managed object's -(BOOL)validate<Key>:error:.
Together, the above two strategies result in unique name-properties in all managed objects.
Maybe I could have forced the NSTextFieldCell to go into editing mode, resulting in -(BOOL)textShouldEndEditing: to get called every time.
Some remarks though:
It seems -(BOOL)textShouldEndEditing: returns NO when the -(BOOL)validate<Key>:error: returns NO.
Both -(BOOL)textShouldEndEditing: and -(BOOL)validate<Key>:error: methods are called only when the user actually makes changes to the property.

Manual binding in Cocoa

I have an ImageView which shows a lock, informing if an opened file is locked or not. I have 2 images for locked and unlocked cases. I want synchronize the displayed image with boolean value of my object representing an opened file.
To do this I want my ViewController to change the image in my ImageView depending on lock state of object. So both object and ViewController have a property "isLocked".
How can I synchronize them? It is easy in IB but I don't know how to do it programmatically. I tried in initialize method of my ViewController to use:
[ViewController bind:#"value" toObject:[ArrayController selection] withKeyPath:#"isLocked" options:nil];
But it doesn't work. In documentation it is said that I have to expose my binding before using it.
I try to put the following code in initializer method of my object:
[self exposeBinding:#"isLocked"];
But Xcode doesn't recognize this method.
Does somebody have experience with this kind of bindings establishing?
As #nick says, you want Key-Value-Observing.
[arrayController addObserver:self
forKeyPath:#"selection.isLocked"
options:NSKeyValueObservingOptionNew
context:#"this_context"]
Then when isLocked changes the -observeValueForKeyPath:ofObject:change:context: method that you have added to your viewController will be called (as long as you only manipulate isLocked in a KVC compliant way).
The options parameter lets you optionally tweak exactly what conditions will trigger the notification and what data is sent along with the notification. The context parameter is there to help you distinguish between notifications that you registered to receive and notifications your superclass registered to receive. It is optional.
Bindings seem like they might be useful to keep two values in sync. However, this is not what they do at all.
Yes, lots of things seem to give the impression that this is what they do, and there isn't much saying that this isn't what they do, also lots of people believe that this is what they do - but no, you cannot use them for this.
Only a handful of classes support bindings (they are listed here) and then, and this is the important bit, those classes only support binding their named bindings, and these bindings are not instance variables. eg NSTextField has a 'fontFamilyName' binding yet NSTextField does not have a 'fontFamilyName' property or instance variable, even a derived one. NSTextField does have a 'isBordered' property but not a binding - so you cannot bind 'isBordered'.
It does not mean anything to 'bind' an arbitrary property of an arbitrary Class.
Yes, you can bind two arbitrary values, the following code works just fine:
#import <Cocoa/Cocoa.h>
#interface SomeObject : NSObject
#property (retain,nonatomic) id someValue;
#end
#implementation SomeObject
#end
int main()
{
SomeObject *source=[SomeObject new];
SomeObject *target=[SomeObject new];
[target bind:#"someValue" toObject:source withKeyPath:#"someValue" options:0];
[source bind:#"someValue" toObject:target withKeyPath:#"someValue" options:0];
[source setSomeValue:#(42)];
NSLog(#"target: %#",[target someValue]);
[target setSomeValue:#(22)];
NSLog(#"source: %#",[source someValue]);
return 0;
}
As far as I can tell, the problem is the bit [ArrayController selection]. The first problem is that ArrayController is (or should be) a class, and getting the class's selection is probably pointless. The other problem is that even if this were an instance, you would be binding to the selection at the time of the call, which is almost certainly not what you want. You want to track the current selection as it changes.
So what you want is probably something like the following:
[myViewController bind:#"value" toObject:myArrayController withKeyPath:#"selection.isLocked" options:nil];

How to use NSWindowDidExposeNotification

I am trying to update another windows when the one becomes visible. So I found the NSWindowDidExposeNotification and tried to work with it, so I wrote in my awakeFromNib:
// MyClass.m
- (void)awakeFromNib {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(mentionsWindowDidExpose:)
name:NSWindowDidExposeNotification
object:nil];
}
and implemented the method
// MyClass.h
- (void)mentionsWindowDidExpose:(id)sender;
// MyClass.m
- (void)mentionsWindowDidExpose:(id)sender {
NSLog(#"test");
}
But it never gets called which is odd. What do I do wrong here?
Generally speaking, you would set up your controller as the window's delegate in order to receive these notifications, like so:
// MyClass.m
- (void)awakeFromNib {
// note: this step can also be done in IB by dragging a connection
// from the window's "delegate" property to your `MyClass` object
[window setDelegate:self];
}
- (void)windowDidExpose:(NSNotification *)notification {
NSLog(#"test");
}
Although, after reading here and here, windowDidExpose may not be your best bet. I would recommend trying the windowDidBecomeKey delegate method instead. That one is posted whenever your window gains "focus" (starts responding to user input) which may be the right time to show your second window.
Update: (in response to comments)
Apple's documentation (quoted below) indicates that NSWindowDidExposeNotification is only valid for nonretained windows, which, according to the posts that I linked above, are quite uncommon.
NSWindowDidExposeNotification
Posted whenever a portion of a nonretained NSWindow object is exposed, whether by being ordered in front of other windows or by other windows being removed from in front of it.
The notification object is the NSWindow object that has been exposed. The userInfo dictionary contains ... the rectangle that has been exposed.
On a higher level, NSNotification objects are simply packages of data that get passed around between Cocoa classes and NSNotificationCenter objects. NSNotificationCenter objects are controllers that manage these packages of data and send them out to observers as required. There is usually no need to trap notifications directly. You can simply use KVC/KVO or pre-defined delegates in your classes and Cocoa handles all of the dirty details behind the scenes.
See Notification Programming Topics and Key Value Coding Programming Guide if you want to know more.

Where do you put cleanup code for NSDocument sub-classes?

I have a document-based application and I have sub-classed NSDocument and provided the required methods, but my document needs some extensive clean-up (needs to run external tasks etc). Where is the best place to put this? I have tried a few different methods such as:
close
close:
canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo
dealloc
If I put it in dealloc, sometimes it gets called and other times it does not (pressing Command+Q seems to bypass my document's deallocation), but it is mandatory that this code gets called without failure (unless program unexpectedly terminates).
Have each document add itself as an observer in the local notification center for NSApplicationWillTerminateNotification. In its notification method, call its clean-up method (which you should also call from dealloc or close).
The correct answer here didn't fit my use case, but the question does. Hence the extra answer.
My use case: closing a document (which may be one of several that are open) but not closing the application.
In this case (at time of writing and unless I'm just looking in the wrong place) the documentation is not as helpful as it could be.
I added a canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo: override in my NSDocument subclass and called super within it. The documentation doesn't say whether you must call super, but a bit of logging shows that the system is providing a selector and a context. This method is called just before the document is closed.
- (void) canCloseDocumentWithDelegate:(id)delegate shouldCloseSelector:(SEL)shouldCloseSelector contextInfo:(void *)contextInfo;
{
if ([self pdfController])
{
[[[self pdfController] window] close];
[self setPdfController: nil];
}
[super canCloseDocumentWithDelegate:delegate shouldCloseSelector: shouldCloseSelector contextInfo: contextInfo];
}
There is some useful discussion of this method on CocoaBuilder. If there's downsides to this approach or better ways of doing this, please comment.

Resources